package services // Live-DB tests for DashboardLayoutService. Skipped when TEST_DATABASE_URL // is unset. import ( "context" "encoding/json" "os" "testing" "github.com/google/uuid" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "mgit.msbls.de/m/paliad/internal/db" ) type dashboardLayoutTestEnv struct { t *testing.T pool *sqlx.DB svc *DashboardLayoutService userID uuid.UUID cleanup func() } func setupDashboardLayoutTest(t *testing.T) *dashboardLayoutTestEnv { 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", err) } if _, err := pool.ExecContext(ctx, `INSERT INTO paliad.users (id, email, display_name, office, global_role) VALUES ($1, $1::text || '@test.local', 'Dashboard Layout Test', 'munich', 'standard') ON CONFLICT (id) DO NOTHING`, userID); err != nil { t.Fatalf("seed paliad.users: %v", err) } cleanup := func() { c := context.Background() pool.ExecContext(c, `DELETE FROM paliad.user_dashboard_layouts WHERE user_id = $1`, userID) pool.ExecContext(c, `DELETE FROM paliad.users WHERE id = $1`, userID) pool.ExecContext(c, `DELETE FROM auth.users WHERE id = $1`, userID) pool.Close() } return &dashboardLayoutTestEnv{ t: t, pool: pool, svc: NewDashboardLayoutService(pool), userID: userID, cleanup: cleanup, } } func TestDashboardLayoutService_GetOrSeedAutoSeeds(t *testing.T) { env := setupDashboardLayoutTest(t) defer env.cleanup() ctx := context.Background() spec, err := env.svc.GetOrSeed(ctx, env.userID) if err != nil { t.Fatalf("GetOrSeed: %v", err) } if spec.Version != LayoutSpecVersion { t.Errorf("seeded version=%d; want %d", spec.Version, LayoutSpecVersion) } if len(spec.Widgets) != len(KnownWidgetKeys) { t.Errorf("seeded widget count=%d; want %d", len(spec.Widgets), len(KnownWidgetKeys)) } // Second call returns the same row, not a second seed. spec2, err := env.svc.GetOrSeed(ctx, env.userID) if err != nil { t.Fatalf("GetOrSeed second: %v", err) } if len(spec2.Widgets) != len(spec.Widgets) { t.Errorf("second call widget count drifted: %d vs %d", len(spec2.Widgets), len(spec.Widgets)) } } func TestDashboardLayoutService_UpdateRoundTrips(t *testing.T) { env := setupDashboardLayoutTest(t) defer env.cleanup() ctx := context.Background() // Seed first so the row exists. if _, err := env.svc.GetOrSeed(ctx, env.userID); err != nil { t.Fatalf("GetOrSeed: %v", err) } // Custom layout: hide matter-summary, reorder. custom := DashboardLayoutSpec{ Version: LayoutSpecVersion, Widgets: []DashboardWidgetRef{ {Key: WidgetUpcomingDeadlines, Visible: true, Settings: json.RawMessage(`{"count": 5, "horizon_days": 14}`)}, {Key: WidgetMatterSummary, Visible: false}, {Key: WidgetDeadlineSummary, Visible: true}, }, } out, err := env.svc.Update(ctx, env.userID, custom) if err != nil { t.Fatalf("Update: %v", err) } if len(out.Widgets) != 3 { t.Fatalf("Update returned %d widgets; want 3", len(out.Widgets)) } if out.Widgets[0].Key != WidgetUpcomingDeadlines { t.Errorf("Update returned widgets[0]=%q; want %q", out.Widgets[0].Key, WidgetUpcomingDeadlines) } if out.Widgets[1].Visible { t.Errorf("Update returned widgets[1].Visible=true; want false") } // Re-read confirms persistence. got, err := env.svc.GetOrSeed(ctx, env.userID) if err != nil { t.Fatalf("GetOrSeed after update: %v", err) } if len(got.Widgets) != 3 { t.Errorf("GetOrSeed after update: %d widgets; want 3", len(got.Widgets)) } } func TestDashboardLayoutService_UpdateRejectsInvalid(t *testing.T) { env := setupDashboardLayoutTest(t) defer env.cleanup() ctx := context.Background() bad := DashboardLayoutSpec{ Version: LayoutSpecVersion, Widgets: []DashboardWidgetRef{ {Key: "fake-widget-key", Visible: true}, }, } if _, err := env.svc.Update(ctx, env.userID, bad); err == nil { t.Fatalf("Update accepted invalid layout") } } func TestDashboardLayoutService_ResetToDefault(t *testing.T) { env := setupDashboardLayoutTest(t) defer env.cleanup() ctx := context.Background() // Custom layout first. custom := DashboardLayoutSpec{ Version: LayoutSpecVersion, Widgets: []DashboardWidgetRef{ {Key: WidgetDeadlineSummary, Visible: true}, }, } if _, err := env.svc.Update(ctx, env.userID, custom); err != nil { t.Fatalf("Update: %v", err) } // Reset. reset, err := env.svc.ResetToDefault(ctx, env.userID) if err != nil { t.Fatalf("ResetToDefault: %v", err) } if len(reset.Widgets) != len(KnownWidgetKeys) { t.Errorf("reset widget count=%d; want %d", len(reset.Widgets), len(KnownWidgetKeys)) } }