Add Cloudflare email and collaboration workflows
This commit is contained in:
157
apps/server/internal/collaboration/repository_test.go
Normal file
157
apps/server/internal/collaboration/repository_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user