158 lines
5.2 KiB
Go
158 lines
5.2 KiB
Go
package collaboration
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"io"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tim/cairnquire/apps/server/internal/auth"
|
|
"github.com/tim/cairnquire/apps/server/internal/config"
|
|
"github.com/tim/cairnquire/apps/server/internal/database"
|
|
)
|
|
|
|
func newCollaborationTestRepository(t *testing.T) (*Repository, *sql.DB) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
db, err := database.Open(ctx, config.DatabaseConfig{Path: filepath.Join(t.TempDir(), "test.sqlite")})
|
|
if err != nil {
|
|
t.Fatalf("open database: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
if err := database.ApplyMigrations(ctx, db.SQL()); err != nil {
|
|
t.Fatalf("apply migrations: %v", err)
|
|
}
|
|
now := formatTime(time.Now().UTC())
|
|
for _, user := range []struct{ id, email, name string }{
|
|
{"user:author", "author@example.com", "Author"},
|
|
{"user:watcher", "watcher@example.com", "Watcher"},
|
|
} {
|
|
if _, err := db.SQL().ExecContext(ctx, `
|
|
INSERT INTO users (id, email, display_name, role, created_at) VALUES (?, ?, ?, 'editor', ?)
|
|
`, user.id, user.email, user.name, now); err != nil {
|
|
t.Fatalf("insert user: %v", err)
|
|
}
|
|
}
|
|
for _, document := range []struct{ id, path, title string }{
|
|
{"doc:guide/setup", "guide/setup.md", "Setup"},
|
|
{"doc:other", "other.md", "Other"},
|
|
} {
|
|
if _, err := db.SQL().ExecContext(ctx, `
|
|
INSERT INTO documents (id, path, current_hash, title, tags, created_at, updated_at)
|
|
VALUES (?, ?, 'hash', ?, '[]', ?, ?)
|
|
`, document.id, document.path, document.title, now, now); err != nil {
|
|
t.Fatalf("insert document: %v", err)
|
|
}
|
|
}
|
|
return NewRepository(db.SQL()), db.SQL()
|
|
}
|
|
|
|
func TestWatcherTargetsAreIdempotentAndFolderMatchingUsesAncestors(t *testing.T) {
|
|
repo, db := newCollaborationTestRepository(t)
|
|
ctx := context.Background()
|
|
now := time.Now().UTC()
|
|
documentID := "doc:guide/setup"
|
|
folder := "guide"
|
|
for range 2 {
|
|
if err := repo.AddWatcher(ctx, Watcher{UserID: "user:watcher", DocumentID: &documentID, CreatedAt: now}); err != nil {
|
|
t.Fatalf("add document watcher: %v", err)
|
|
}
|
|
if err := repo.AddWatcher(ctx, Watcher{UserID: "user:watcher", FolderPath: &folder, CreatedAt: now}); err != nil {
|
|
t.Fatalf("add folder watcher: %v", err)
|
|
}
|
|
}
|
|
var count int
|
|
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM watchers`).Scan(&count); err != nil {
|
|
t.Fatalf("count watchers: %v", err)
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("watcher count = %d, want 2", count)
|
|
}
|
|
|
|
tests := []struct {
|
|
path string
|
|
want int
|
|
}{
|
|
{"guide/setup.md", 1},
|
|
{"guide/deep/setup.md", 1},
|
|
{"guidebook/setup.md", 0},
|
|
{"other.md", 0},
|
|
}
|
|
for _, test := range tests {
|
|
watchers, err := repo.ListWatchersByFolder(ctx, test.path)
|
|
if err != nil {
|
|
t.Fatalf("ListWatchersByFolder(%q): %v", test.path, err)
|
|
}
|
|
if len(watchers) != test.want {
|
|
t.Errorf("ListWatchersByFolder(%q) = %d, want %d", test.path, len(watchers), test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommentReplyValidationHistoryAndThreadResolution(t *testing.T) {
|
|
repo, _ := newCollaborationTestRepository(t)
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
service := NewServiceWithOptions(repo, nil, logger, Options{})
|
|
ctx := context.Background()
|
|
principal := auth.Principal{UserID: "user:author", DisplayName: "Author"}
|
|
anchor := "anchor"
|
|
|
|
root, err := service.CreateComment(ctx, principal, CreateCommentInput{
|
|
DocumentID: "doc:guide/setup", VersionHash: "hash", Content: "Root", AnchorHash: &anchor,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create root: %v", err)
|
|
}
|
|
reply, err := service.CreateComment(ctx, principal, CreateCommentInput{
|
|
DocumentID: "doc:guide/setup", VersionHash: "hash", Content: "Reply", ParentID: &root.ID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create reply: %v", err)
|
|
}
|
|
if reply.AnchorHash == nil || *reply.AnchorHash != anchor {
|
|
t.Fatalf("reply anchor = %#v, want inherited %q", reply.AnchorHash, anchor)
|
|
}
|
|
grandchild, err := service.CreateComment(ctx, principal, CreateCommentInput{
|
|
DocumentID: "doc:guide/setup", VersionHash: "hash", Content: "Grandchild", ParentID: &reply.ID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create grandchild: %v", err)
|
|
}
|
|
if _, err := service.CreateComment(ctx, principal, CreateCommentInput{
|
|
DocumentID: "doc:other", VersionHash: "hash", Content: "Wrong document", ParentID: &root.ID,
|
|
}); err == nil {
|
|
t.Fatal("cross-document reply should fail")
|
|
}
|
|
|
|
if err := service.ResolveComment(ctx, principal, root.ID); err != nil {
|
|
t.Fatalf("resolve thread: %v", err)
|
|
}
|
|
active, err := service.ListCommentsByAnchor(ctx, "doc:guide/setup", anchor, false)
|
|
if err != nil {
|
|
t.Fatalf("list active: %v", err)
|
|
}
|
|
if len(active) != 0 {
|
|
t.Fatalf("active comments = %d, want 0", len(active))
|
|
}
|
|
history, err := service.ListCommentsByAnchor(ctx, "doc:guide/setup", anchor, true)
|
|
if err != nil {
|
|
t.Fatalf("list history: %v", err)
|
|
}
|
|
if len(history) != 3 {
|
|
t.Fatalf("history comments = %d, want 3", len(history))
|
|
}
|
|
for _, comment := range history {
|
|
if comment.ResolvedAt == nil {
|
|
t.Errorf("comment %s was not resolved", comment.ID)
|
|
}
|
|
}
|
|
if _, err := service.CreateComment(ctx, principal, CreateCommentInput{
|
|
DocumentID: "doc:guide/setup", VersionHash: "hash", Content: "Too late", ParentID: &grandchild.ID,
|
|
}); err == nil {
|
|
t.Fatal("reply to resolved thread should fail")
|
|
}
|
|
}
|