package opsstatus_test import ( "context" "database/sql" "net/url" "testing" "time" "galaxy/backend/internal/mail" "galaxy/backend/internal/opsstatus" backendpg "galaxy/backend/internal/postgres" pgshared "galaxy/postgres" "github.com/google/uuid" testcontainers "github.com/testcontainers/testcontainers-go" tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/wait" ) const ( pgImage = "postgres:16-alpine" pgUser = "galaxy" pgPassword = "galaxy" pgDatabase = "galaxy_backend" pgSchema = "backend" pgStartup = 90 * time.Second pgOpTO = 10 * time.Second ) // startPostgres mirrors the per-package scaffolding used by the other store // tests: spin up Postgres, apply migrations, return *sql.DB. func startPostgres(t *testing.T) *sql.DB { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) t.Cleanup(cancel) pgContainer, err := tcpostgres.Run(ctx, pgImage, tcpostgres.WithDatabase(pgDatabase), tcpostgres.WithUsername(pgUser), tcpostgres.WithPassword(pgPassword), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(pgStartup), ), ) if err != nil { t.Skipf("postgres testcontainer unavailable, skipping: %v", err) } t.Cleanup(func() { if termErr := testcontainers.TerminateContainer(pgContainer); termErr != nil { t.Errorf("terminate postgres container: %v", termErr) } }) baseDSN, err := pgContainer.ConnectionString(ctx, "sslmode=disable") if err != nil { t.Fatalf("connection string: %v", err) } scopedDSN, err := dsnWithSearchPath(baseDSN, pgSchema) if err != nil { t.Fatalf("scope dsn: %v", err) } cfg := pgshared.DefaultConfig() cfg.PrimaryDSN = scopedDSN cfg.OperationTimeout = pgOpTO db, err := pgshared.OpenPrimary(ctx, cfg, backendpg.NoObservabilityOptions()...) if err != nil { t.Fatalf("open primary: %v", err) } t.Cleanup(func() { _ = db.Close() }) if err := pgshared.Ping(ctx, db, cfg.OperationTimeout); err != nil { t.Fatalf("ping: %v", err) } if err := backendpg.ApplyMigrations(ctx, db); err != nil { t.Fatalf("apply migrations: %v", err) } return db } func dsnWithSearchPath(baseDSN, schema string) (string, error) { parsed, err := url.Parse(baseDSN) if err != nil { return "", err } values := parsed.Query() values.Set("search_path", schema) if values.Get("sslmode") == "" { values.Set("sslmode", "disable") } parsed.RawQuery = values.Encode() return parsed.String(), nil } func TestStoreCollect(t *testing.T) { db := startPostgres(t) store := opsstatus.NewStore(db) ctx := context.Background() // Empty schema: queries must execute cleanly with zero counts. empty := store.Collect(ctx) if !empty.PostgresHealthy { t.Fatal("PostgresHealthy must be true against a reachable database") } if len(empty.Errors) != 0 { t.Fatalf("unexpected collection errors: %v", empty.Errors) } if got := totalCount(empty.MailDeliveries); got != 0 { t.Fatalf("mail deliveries total = %d, want 0", got) } if len(empty.Runtimes) != 0 || len(empty.NotificationRoutes) != 0 { t.Fatalf("expected empty status slices, got runtimes=%v routes=%v", empty.Runtimes, empty.NotificationRoutes) } if empty.NotificationMalformed != 0 { t.Fatalf("malformed notifications = %d, want 0", empty.NotificationMalformed) } // Enqueue one mail delivery and confirm the GROUP BY count reflects it. mailStore := mail.NewStore(db) inserted, err := mailStore.InsertEnqueue(ctx, mail.EnqueueArgs{ DeliveryID: uuid.New(), TemplateID: mail.TemplateLoginCode, IdempotencyKey: uuid.NewString(), Recipients: []string{"ops@example.test"}, ContentType: "text/plain", Subject: "hello", Body: []byte("hi"), }) if err != nil { t.Fatalf("insert mail delivery: %v", err) } if !inserted { t.Fatal("expected the delivery to be inserted") } after := store.Collect(ctx) if len(after.Errors) != 0 { t.Fatalf("unexpected collection errors after insert: %v", after.Errors) } if got := totalCount(after.MailDeliveries); got != 1 { t.Fatalf("mail deliveries total after insert = %d, want 1 (statuses: %v)", got, after.MailDeliveries) } } func totalCount(counts []opsstatus.StatusCount) int64 { var total int64 for _, c := range counts { total += c.Count } return total }