// Package testdb provides shared fixtures for integration tests that need a // real Postgres connection. It targets TEST_DATABASE_URL, which `make test` // points at a dedicated inventory_2_test database by default; override it // (e.g. `make test TEST_DATABASE_URL=...`) to run the same tests against // another database, such as the real dev one. package testdb import ( "context" "io" "log/slog" "os" "testing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "ruben/inventory2/logging" ) // Pool connects to TEST_DATABASE_URL. If it isn't set, the test is skipped // rather than failed, so `go test ./...` works without Postgres running. func Pool(t *testing.T) *pgxpool.Pool { t.Helper() dsn := os.Getenv("TEST_DATABASE_URL") if dsn == "" { t.Skip("TEST_DATABASE_URL not set; skipping integration test (see .env.example, or run via `make test`)") } pool, err := pgxpool.New(context.Background(), dsn) if err != nil { t.Fatalf("failed to connect to test database: %v", err) } t.Cleanup(pool.Close) if err := pool.Ping(context.Background()); err != nil { t.Fatalf("failed to reach test database at TEST_DATABASE_URL: %v", err) } return pool } // Logger returns a logger that discards output, for stores that require one // but whose logging isn't under test. func Logger() *logging.Logger { return logging.New(slog.NewTextHandler(io.Discard, nil)) } // NewUserID returns a unique user_id for a test to use, so parallel test // runs (including against a shared database) never collide. func NewUserID(t *testing.T) string { t.Helper() return "test-" + t.Name() + "-" + uuid.NewString() } // SeedOAuthUser inserts a bare oauth_users row so accounts/etc. FKs that // reference it are satisfiable, and registers cleanup. Use SeedOAuthSession // instead if the test also needs a valid access token. func SeedOAuthUser(t *testing.T, pool *pgxpool.Pool, userID string) { t.Helper() ctx := context.Background() if _, err := pool.Exec(ctx, `INSERT INTO oauth_users (user_id) VALUES ($1)`, userID); err != nil { t.Fatalf("failed to seed oauth_users row: %v", err) } t.Cleanup(func() { if _, err := pool.Exec(context.Background(), `DELETE FROM oauth_users WHERE user_id = $1`, userID); err != nil { t.Errorf("cleanup: failed to delete oauth_users row: %v", err) } }) } // SeedOAuthSession inserts an oauth_users row plus a matching oauth_tokens // row with a freshly generated access token, mirroring the shape a real (or // authentication.Authenticator.DevLogin) session leaves behind. Registers // cleanup for both rows, in dependency order. func SeedOAuthSession(t *testing.T, pool *pgxpool.Pool, userID string) (accessToken string) { t.Helper() ctx := context.Background() SeedOAuthUser(t, pool, userID) accessToken = "test-token-" + uuid.NewString() _, err := pool.Exec(ctx, ` INSERT INTO oauth_tokens ( access_token, token_type, refresh_token, expiry, id_token_issuer, id_token_audience, id_token_subject, id_token_expiry, id_token_issued_at, id_token_nonce, id_token_access_token_hash, id_token_custom_claims_family_name, id_token_custom_claims_given_name, id_token_custom_claims_name, id_token_custom_claims_nickname, id_token_custom_claims_picture, id_token_custom_claims_updated_at ) VALUES ( $1, 'test', '', NOW() + INTERVAL '1 hour', 'test', '{test}', $2, NOW() + INTERVAL '1 hour', NOW(), '', '', 'Test', 'User', 'Test User', 'testuser', '', NOW() ) `, accessToken, userID) if err != nil { t.Fatalf("failed to seed oauth_tokens row: %v", err) } t.Cleanup(func() { if _, err := pool.Exec(context.Background(), `DELETE FROM oauth_tokens WHERE access_token = $1`, accessToken); err != nil { t.Errorf("cleanup: failed to delete oauth_tokens row: %v", err) } }) return accessToken }