207 lines
5.9 KiB
Go
207 lines
5.9 KiB
Go
package docs
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"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 TestSaveSourcePageWithBaseHashCanResolveWithLatestHash(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\nQueued offline edit", page.Hash)
|
|
var conflict *DocumentConflictError
|
|
if !errors.As(err, &conflict) {
|
|
t.Fatalf("SaveSourcePageWithBaseHash() error = %v, want DocumentConflictError", err)
|
|
}
|
|
|
|
resolvedContent := conflict.CurrentContent + "\n\nResolved local addition"
|
|
updated, err := service.SaveSourcePageWithBaseHash(ctx, "hello", resolvedContent, conflict.CurrentHash)
|
|
if err != nil {
|
|
t.Fatalf("SaveSourcePageWithBaseHash() latest hash error = %v", err)
|
|
}
|
|
if updated.Hash == conflict.CurrentHash {
|
|
t.Fatal("expected resolved save to produce a new hash")
|
|
}
|
|
|
|
content, err := os.ReadFile(filepath.Join(sourceDir, "hello.md"))
|
|
if err != nil {
|
|
t.Fatalf("read hello.md: %v", err)
|
|
}
|
|
if string(content) != resolvedContent {
|
|
t.Fatalf("resolved content = %q, want %q", string(content), resolvedContent)
|
|
}
|
|
}
|
|
|
|
func TestSyncSourceDirHandles100FilesUnderTarget(t *testing.T) {
|
|
service, sourceDir := setupDocsTestService(t)
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 100; i++ {
|
|
name := filepath.Join(sourceDir, fmt.Sprintf("note-%03d.md", i))
|
|
content := fmt.Sprintf("# Note %03d\n\nDeterministic body %03d\n", i, i)
|
|
if err := os.WriteFile(name, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("create %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
start := time.Now()
|
|
changes, err := service.SyncSourceDir(ctx)
|
|
if err != nil {
|
|
t.Fatalf("SyncSourceDir() error = %v", err)
|
|
}
|
|
if elapsed := time.Since(start); elapsed > 10*time.Second {
|
|
t.Fatalf("SyncSourceDir() took %s, want <= 10s", elapsed)
|
|
}
|
|
if len(changes) != 101 {
|
|
t.Fatalf("changes = %d, want 101", len(changes))
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|