package docs import ( "context" "database/sql" "errors" "log/slog" "os" "path/filepath" "reflect" "testing" "github.com/tim/cairnquire/apps/server/internal/database" "github.com/tim/cairnquire/apps/server/internal/markdown" "github.com/tim/cairnquire/apps/server/internal/store" ) func TestNormalizeRequestPathCandidatesSupportsIndexFiles(t *testing.T) { tests := []struct { name string path string want []string }{ { name: "root", path: "", want: []string{"index.md", "getting-started.md"}, }, { name: "clean folder or page path", path: "guide", want: []string{"guide.md", "guide/index.md"}, }, { name: "explicit markdown path", path: "guide/index.md", want: []string{"guide/index.md"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := normalizeRequestPathCandidates(tt.path) if !reflect.DeepEqual(got, tt.want) { t.Fatalf("normalizeRequestPathCandidates(%q) = %#v, want %#v", tt.path, got, tt.want) } }) } } func TestSaveSourcePageWithBaseHashRejectsStaleEditor(t *testing.T) { service, sourceDir := setupDocsTestService(t) ctx := context.Background() page, err := service.LoadSourcePage(ctx, "hello") if err != nil { t.Fatalf("LoadSourcePage() error = %v", err) } serverContent := "# Hello\n\nServer edit" if err := os.WriteFile(filepath.Join(sourceDir, "hello.md"), []byte(serverContent), 0o644); err != nil { t.Fatalf("write server edit: %v", err) } _, err = service.SaveSourcePageWithBaseHash(ctx, "hello", "# Hello\n\nClient edit", page.Hash) var conflict *DocumentConflictError if !errors.As(err, &conflict) { t.Fatalf("SaveSourcePageWithBaseHash() error = %v, want DocumentConflictError", err) } if conflict.BaseHash != page.Hash { t.Fatalf("conflict base hash = %q, want %q", conflict.BaseHash, page.Hash) } if conflict.CurrentContent != serverContent { t.Fatalf("conflict current content = %q, want %q", conflict.CurrentContent, serverContent) } content, err := os.ReadFile(filepath.Join(sourceDir, "hello.md")) if err != nil { t.Fatalf("read hello.md: %v", err) } if string(content) != serverContent { t.Fatalf("stale save overwrote content: %q", string(content)) } } func TestSaveSourcePageWithBaseHashAcceptsMatchingHash(t *testing.T) { service, sourceDir := setupDocsTestService(t) ctx := context.Background() page, err := service.LoadSourcePage(ctx, "hello") if err != nil { t.Fatalf("LoadSourcePage() error = %v", err) } nextContent := "# Hello\n\nClient edit" updated, err := service.SaveSourcePageWithBaseHash(ctx, "hello", nextContent, page.Hash) if err != nil { t.Fatalf("SaveSourcePageWithBaseHash() error = %v", err) } if updated.Hash == page.Hash { t.Fatal("expected hash to change after save") } content, err := os.ReadFile(filepath.Join(sourceDir, "hello.md")) if err != nil { t.Fatalf("read hello.md: %v", err) } if string(content) != nextContent { t.Fatalf("saved content = %q, want %q", string(content), nextContent) } } func setupDocsTestService(t *testing.T) (*Service, string) { t.Helper() sourceDir := t.TempDir() storeDir := t.TempDir() if err := os.WriteFile(filepath.Join(sourceDir, "hello.md"), []byte("# Hello\n\nOriginal"), 0o644); err != nil { t.Fatalf("create hello.md: %v", err) } dbPath := filepath.Join(t.TempDir(), "test.db") db, err := sql.Open("libsql", "file:"+dbPath) if err != nil { t.Fatalf("open database: %v", err) } t.Cleanup(func() { db.Close() }) ctx := context.Background() if err := database.ApplyMigrations(ctx, db); err != nil { t.Fatalf("apply migrations: %v", err) } contentStore, err := store.New(storeDir) if err != nil { t.Fatalf("create content store: %v", err) } repo := NewRepository(db) return NewService(sourceDir, contentStore, markdown.NewRenderer(), repo, slog.Default()), sourceDir }