package services // Live-DB tests for UserViewService. Skipped when TEST_DATABASE_URL is // unset, mirroring the rest of the live-DB test suite. import ( "context" "errors" "os" "testing" "github.com/google/uuid" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "mgit.msbls.de/m/paliad/internal/db" ) type userViewTestEnv struct { t *testing.T pool *sqlx.DB svc *UserViewService userID uuid.UUID cleanup func() } func setupUserViewTest(t *testing.T) *userViewTestEnv { t.Helper() url := os.Getenv("TEST_DATABASE_URL") if url == "" { t.Skip("TEST_DATABASE_URL not set — skipping live DB test") } if err := db.ApplyMigrations(url); err != nil { t.Fatalf("apply migrations: %v", err) } pool, err := sqlx.Connect("postgres", url) if err != nil { t.Fatalf("connect: %v", err) } ctx := context.Background() userID := uuid.New() if _, err := pool.ExecContext(ctx, `INSERT INTO auth.users (id, email) VALUES ($1, $1::text || '@test.local') ON CONFLICT (id) DO NOTHING`, userID); err != nil { t.Logf("skip auth.users seed: %v (continuing — auth schema may be locked down)", err) } if _, err := pool.ExecContext(ctx, `INSERT INTO paliad.users (id, email, display_name, office, global_role) VALUES ($1, $1::text || '@test.local', 'View Test User', 'munich', 'standard') ON CONFLICT (id) DO NOTHING`, userID); err != nil { t.Fatalf("seed paliad.users: %v", err) } cleanup := func() { ctx := context.Background() pool.ExecContext(ctx, `DELETE FROM paliad.user_views WHERE user_id = $1`, userID) pool.ExecContext(ctx, `DELETE FROM paliad.users WHERE id = $1`, userID) pool.ExecContext(ctx, `DELETE FROM auth.users WHERE id = $1`, userID) pool.Close() } return &userViewTestEnv{ t: t, pool: pool, svc: NewUserViewService(pool), userID: userID, cleanup: cleanup, } } func goodCreateInput(slug, name string) CreateUserViewInput { return CreateUserViewInput{ Slug: slug, Name: name, FilterSpec: DefaultFilterSpec(), RenderSpec: DefaultRenderSpec(), } } func TestUserViewService_CreateAndList(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() created, err := env.svc.Create(ctx, env.userID, goodCreateInput("freitag-stand", "Freitag-Stand")) if err != nil { t.Fatalf("create: %v", err) } if created.Slug != "freitag-stand" || created.Name != "Freitag-Stand" { t.Errorf("created shape: %+v", created) } if created.SortOrder != 0 { t.Errorf("first view sort_order = %d, want 0", created.SortOrder) } second, err := env.svc.Create(ctx, env.userID, goodCreateInput("siemens", "Siemens-Aktivität")) if err != nil { t.Fatalf("second create: %v", err) } if second.SortOrder != 1 { t.Errorf("second view sort_order = %d, want 1", second.SortOrder) } list, err := env.svc.ListForUser(ctx, env.userID) if err != nil { t.Fatalf("list: %v", err) } if len(list) != 2 { t.Fatalf("expected 2 views, got %d", len(list)) } // sort_order ASC ordering if list[0].Slug != "freitag-stand" || list[1].Slug != "siemens" { t.Errorf("ordering: %v", []string{list[0].Slug, list[1].Slug}) } } func TestUserViewService_GetBySlugAndID(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() created, err := env.svc.Create(ctx, env.userID, goodCreateInput("test-view", "Test View")) if err != nil { t.Fatalf("create: %v", err) } bySlug, err := env.svc.GetBySlug(ctx, env.userID, "test-view") if err != nil || bySlug == nil { t.Fatalf("GetBySlug: %v / nil", err) } if bySlug.ID != created.ID { t.Errorf("GetBySlug id mismatch") } byID, err := env.svc.GetByID(ctx, env.userID, created.ID) if err != nil || byID == nil { t.Fatalf("GetByID: %v / nil", err) } missing, err := env.svc.GetBySlug(ctx, env.userID, "does-not-exist") if err != nil { t.Fatalf("GetBySlug missing: %v", err) } if missing != nil { t.Error("missing slug should return nil") } } func TestUserViewService_SlugUniquenessPerUser(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() if _, err := env.svc.Create(ctx, env.userID, goodCreateInput("dup", "First")); err != nil { t.Fatalf("first create: %v", err) } _, err := env.svc.Create(ctx, env.userID, goodCreateInput("dup", "Second")) if !errors.Is(err, ErrUserViewSlugTaken) { t.Fatalf("duplicate slug must return ErrUserViewSlugTaken, got %v", err) } } func TestUserViewService_Update(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() created, err := env.svc.Create(ctx, env.userID, goodCreateInput("orig", "Original")) if err != nil { t.Fatalf("create: %v", err) } newName := "Updated" newShowCount := true updated, err := env.svc.Update(ctx, env.userID, created.ID, UpdateUserViewInput{ Name: &newName, ShowCount: &newShowCount, }) if err != nil { t.Fatalf("update: %v", err) } if updated.Name != "Updated" { t.Errorf("name not updated: %s", updated.Name) } if !updated.ShowCount { t.Errorf("show_count not updated") } // Slug should be unchanged. if updated.Slug != "orig" { t.Errorf("slug should be unchanged, got %s", updated.Slug) } } func TestUserViewService_UpdateRejectsReservedSlug(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() created, err := env.svc.Create(ctx, env.userID, goodCreateInput("freely", "Freely")) if err != nil { t.Fatalf("create: %v", err) } reserved := "dashboard" _, err = env.svc.Update(ctx, env.userID, created.ID, UpdateUserViewInput{Slug: &reserved}) if !errors.Is(err, ErrInvalidInput) { t.Fatalf("update to reserved slug must reject, got %v", err) } } func TestUserViewService_Delete(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() created, err := env.svc.Create(ctx, env.userID, goodCreateInput("doomed", "Doomed")) if err != nil { t.Fatalf("create: %v", err) } deleted, err := env.svc.Delete(ctx, env.userID, created.ID) if err != nil || !deleted { t.Fatalf("delete: %v, %v", deleted, err) } deletedAgain, err := env.svc.Delete(ctx, env.userID, created.ID) if err != nil { t.Fatalf("second delete: %v", err) } if deletedAgain { t.Error("second delete should report not-deleted") } } func TestUserViewService_TouchAndMostRecent(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() a, _ := env.svc.Create(ctx, env.userID, goodCreateInput("a-view", "A")) b, _ := env.svc.Create(ctx, env.userID, goodCreateInput("b-view", "B")) // Before any touch — MostRecent is nil. mr, err := env.svc.MostRecent(ctx, env.userID) if err != nil { t.Fatalf("most_recent: %v", err) } if mr != nil { t.Errorf("MostRecent should be nil before any touch") } if err := env.svc.Touch(ctx, env.userID, a.ID); err != nil { t.Fatalf("touch a: %v", err) } if err := env.svc.Touch(ctx, env.userID, b.ID); err != nil { t.Fatalf("touch b: %v", err) } mr, err = env.svc.MostRecent(ctx, env.userID) if err != nil || mr == nil { t.Fatalf("most_recent after touch: %v / nil", err) } if mr.ID != b.ID { t.Errorf("most-recent should be b (touched last), got %s", mr.Slug) } } func TestUserViewService_RejectsReservedSlugOnCreate(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() _, err := env.svc.Create(ctx, env.userID, goodCreateInput("inbox", "Inbox copy")) if !errors.Is(err, ErrInvalidInput) { t.Fatalf("reserved slug on create must reject, got %v", err) } } func TestUserViewService_RejectsBadSlug(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() _, err := env.svc.Create(ctx, env.userID, goodCreateInput("Has Spaces", "Bad")) if !errors.Is(err, ErrInvalidInput) { t.Fatalf("slug with spaces must reject, got %v", err) } _, err = env.svc.Create(ctx, env.userID, goodCreateInput("UPPER", "Bad")) if !errors.Is(err, ErrInvalidInput) { t.Fatalf("uppercase slug must reject, got %v", err) } } func TestUserViewService_RejectsEmptyName(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() _, err := env.svc.Create(ctx, env.userID, CreateUserViewInput{ Slug: "no-name", Name: "", FilterSpec: DefaultFilterSpec(), RenderSpec: DefaultRenderSpec(), }) if !errors.Is(err, ErrInvalidInput) { t.Fatalf("empty name must reject, got %v", err) } } func TestUserViewService_RejectsInvalidSpec(t *testing.T) { env := setupUserViewTest(t) defer env.cleanup() ctx := context.Background() bad := DefaultFilterSpec() bad.Sources = nil _, err := env.svc.Create(ctx, env.userID, CreateUserViewInput{ Slug: "bad-spec", Name: "Bad", FilterSpec: bad, RenderSpec: DefaultRenderSpec(), }) if !errors.Is(err, ErrInvalidInput) { t.Fatalf("invalid spec must reject, got %v", err) } }