package dashboard import ( "context" "errors" "os" "testing" "time" "github.com/itworx/pulse/internal/database" ) func TestDashboardRepositoryPostgreSQL(t *testing.T) { dsn := os.Getenv("PULSE_TEST_DATABASE_URL") if dsn == "" { t.Skip("PULSE_TEST_DATABASE_URL is not set") } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() pool, err := database.NewPool(ctx, database.Config{URL: dsn, MaxConns: 10, MinConns: 1}) if err != nil { t.Fatal(err) } defer pool.Close() if err := database.Migrate(ctx, pool); err != nil { t.Fatal(err) } actor := "00000000-0000-0000-0000-0000000003a1" otherActor := "00000000-0000-0000-0000-0000000003a2" if _, err := pool.Exec(ctx, "INSERT INTO users (id, external_subject, display_name) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING", actor, actor, "M3 actor"); err != nil { t.Fatal(err) } if _, err := pool.Exec(ctx, "INSERT INTO users (id, external_subject, display_name) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING", otherActor, otherActor, "M3 other actor"); err != nil { t.Fatal(err) } repo := Repository{Pool: pool} id := "00000000-0000-0000-0000-0000000003d1" _, _ = pool.Exec(ctx, "DELETE FROM dashboards WHERE id=$1 OR slug IN ('m3-repo','m3-repo-copy')", id) doc := Document{"schemaVersion": 2, "id": id, "slug": "m3-repo", "name": "M3 Repo", "scope": "personal", "variables": []any{}, "widgets": []any{}, "settings": map[string]any{"refreshSeconds": 30}} summary, version, err := repo.Create(ctx, actor, doc, "initial") if err != nil { t.Fatal(err) } if version.Number != 1 || summary.Revision != 1 { t.Fatalf("create=%+v/%+v", summary, version) } same, err := repo.UpdateDocument(ctx, id, actor, 1, doc, "noop") if err != nil || same.Revision != 1 { t.Fatalf("noop=%+v err=%v", same, err) } doc["name"] = "Changed" updated, err := repo.UpdateDocument(ctx, id, actor, 1, doc, "edit") if err != nil || updated.Revision != 2 || updated.CurrentVersion != 2 { t.Fatalf("update=%+v err=%v", updated, err) } if _, err := repo.UpdateDocument(ctx, id, actor, 1, doc, "stale"); !errors.Is(err, ErrConflict) { t.Fatalf("expected conflict, got %v", err) } restored, err := repo.Restore(ctx, id, actor, 2, 1) if err != nil || restored.Revision != 3 || restored.CurrentVersion != 3 { t.Fatalf("restore=%+v err=%v", restored, err) } versions, err := repo.Versions(ctx, id, actor, 10) if err != nil || len(versions) != 3 { t.Fatalf("versions=%d err=%v", len(versions), err) } historical, err := repo.GetVersion(ctx, id, actor, 1) if err != nil || historical.Number != 1 { t.Fatalf("historical=%+v err=%v", historical, err) } if _, _, err := repo.Get(ctx, id, otherActor); !errors.Is(err, ErrNotFound) { t.Fatalf("personal dashboard leaked through direct read: %v", err) } if otherVersions, err := repo.Versions(ctx, id, otherActor, 10); err != nil || len(otherVersions) != 0 { t.Fatalf("personal dashboard versions leaked: count=%d err=%v", len(otherVersions), err) } if _, err := repo.GetVersion(ctx, id, otherActor, 1); !errors.Is(err, ErrNotFound) { t.Fatalf("personal dashboard version leaked through direct read: %v", err) } if _, _, err := repo.Create(ctx, actor, doc, "duplicate"); !errors.Is(err, ErrConflict) { t.Fatalf("expected duplicate conflict, got %v", err) } cloned, clonedVersion, err := repo.Clone(ctx, actor, id, "m3-repo-copy", "M3 Repo Copy") if err != nil || cloned.ID == id || clonedVersion.Number != 1 || cloned.Scope != "personal" { t.Fatalf("clone=%+v/%+v err=%v", cloned, clonedVersion, err) } listed, err := repo.List(ctx, actor, 10) if err != nil || len(listed) < 2 { t.Fatalf("list=%d err=%v", len(listed), err) } edited, err := repo.UpdateMetadata(ctx, id, actor, 3, "M3 Repo Renamed", "updated") if err != nil || edited.Revision != 4 { t.Fatalf("metadata=%+v err=%v", edited, err) } archived, err := repo.Archive(ctx, id, actor, 4) if err != nil || archived.ArchivedAt == nil || archived.Revision != 5 { t.Fatalf("archive=%+v err=%v", archived, err) } if _, err := pool.Exec(ctx, "UPDATE dashboard_versions SET change_summary='bad' WHERE dashboard_id=$1", id); err == nil { t.Fatal("expected immutable version failure") } if _, err := pool.Exec(ctx, "DELETE FROM dashboards WHERE id=$1", id); err != nil { t.Fatal(err) } if _, err := pool.Exec(ctx, "DELETE FROM dashboards WHERE id=$1", cloned.ID); err != nil { t.Fatal(err) } }