diff --git a/Makefile b/Makefile index 3500623..3eb5345 100644 --- a/Makefile +++ b/Makefile @@ -180,11 +180,10 @@ test-integration: ## T1 integration tier - needs Postgres; starts postgres-test @go test -tags integration -p 1 -parallel $$(./scripts/test-db-prepare.sh --print-parallel) \ ./cmd/... ./internal/... ./tests/... @echo "" - @echo "$(YELLOW)Note: the not-yet-migrated files under tests/integration also want$(RESET)" - @echo "$(YELLOW)a PDS and Jetstream, and they still SKIP themselves when those are$(RESET)" - @echo "$(YELLOW)missing — so a green run here does not mean the suite ran in full.$(RESET)" - @echo "$(YELLOW)Run 'make dev-up' first for the fuller local picture, and 'make ci'$(RESET)" - @echo "$(YELLOW)for the gate that refuses to count a skip as a pass.$(RESET)" + @echo "$(YELLOW)Note: tests/integration needs a PDS and Jetstream as well as Postgres,$(RESET)" + @echo "$(YELLOW)and its TestMain now says so up front — without 'make dev-up' the$(RESET)" + @echo "$(YELLOW)package fails once, naming the address it could not reach, instead of$(RESET)" + @echo "$(YELLOW)skipping test by test and reporting green. 'make ci' remains the gate.$(RESET)" test-e2e: ## T2 pipeline tier - needs the dev stack AND a running AppView @# TRANSITIONAL. docs/TEST_ARCHITECTURE.md §3.5 puts this tier inside the diff --git a/loop_state.md b/loop_state.md index bb41b54..9fb5486 100644 --- a/loop_state.md +++ b/loop_state.md @@ -44,7 +44,7 @@ Stop the loop when every task is done, or on any blocked task. | 5 | Kill the lies: delete 6 debt tests; lexicon validator stops generating defs-only subtests (retire 8 allowlist entries); move 2 ratelimit files to internal/api/middleware (T0); fold tests/unit into internal/core/communities | 2 | M | done | (see git log) | ALLOWLIST → 0 ENTRIES; make ci GREEN 3399/3399, 0 skips. Lexicon fix is a coverage GAIN (43 defs-only fragment resolutions previously asserted nothing + two-way naming consistency). tests/unit was 100% FAKE (servers never dialed, literals asserted against themselves, t.Log theater) — deleted wholesale, nothing to port; communities now at honest zero (task 17). 3 tautology "tests" deleted rather than moved. Audit 911→897 | | 6 | Split multi-tier files by test func (manifest in commit msg); add build tags in place; retarget Makefile to tags; delete -short/testing.Short(); delete test-all | 2 ⛩ | S | done | (see git log) | PHASE 2 COMPLETE. 76 files `integration`, 3 `e2e`; 2 jetstream files split; 161 Short guards deleted (162nd was a doc comment); test-all + 4 dead targets gone. Honesty test: untagged suite green under --network none FIRST TRY (36 pkgs). make test = 11s no-Docker. Review (Codex needs-work / Opus safe-as-is): 8 fixes — GATE INTEGRITY closed (exit codes captured + mismatch rule; OOM-137-with-green-report now fails — was a silent pass since the harness was born; proved via truth table), -parallel 1 pinned on e2e (serial T2), readiness probe now hits the HOST endpoint tests dial, shared-DB migrate restored via testkit.MigrateSharedDatabase (advisory-locked, in testdbprepare), DSN redacted via url.Redacted, pure testkit files untagged (TestMain split into tagged harness_test.go + untagged harness_support_test.go), T0 socket-free (failingTransport). make ci GREEN 3399/0 skips 2m4s; audit 573 | | 7 | Migrate setupTestDB call sites → testkit.DB(t), batch 1 (~25 files) + delete their DELETE FROMs/cleanups | 3 | M | done | (see git log) | 29 files (aggregator_e2e..concurrent_scenarios incl. 4 hand-rolled setup clones), 115 sites → testkit.DB(t) (2 needed NO db at all — migration doubles as unused-DB detector), 19 DELETE FROMs + 1 cleanup fn deleted, diff +142/−828. Isolation PROVEN: concurrent -count=2 on a hardcoded-PK pair green; 0 leaked clones. make ci GREEN 3399/0 @142s (+18s vs baseline: ~150ms/test = FORCE-drop + 2 lock RTs — task 9 pays it back). Audit 573→561. NO order-dependency failures surfaced | -| 8 | Migrate remaining call sites; delete all 3 setupTestDB defs + per-file cleanup fns | 3 | M | pending | | | +| 8 | Migrate remaining call sites; delete all 3 setupTestDB defs + per-file cleanup fns | 3 | M | done | (see git log) | DB MIGRATION COMPLETE: 128 sites (31 files incl. live+e2e), all 4 defs + 4 cleanup fns + 18 goose pairs + 63 wipes deleted, +189/−1182. grep setupTestDB|goose in tests/ = EMPTY. e2e shared-DB hazard was HYPOTHETICAL (user_signup setupTestDB had ZERO callers; error_recovery all in-process) — SharedDB not needed. TestMain → testkit.Main(RequirePostgres, RequirePDS, RequireJetstream): make test-integration now FAILS without dev stack instead of skip-green (spec-honest, kept). FULL -shuffle=on INTEGRATION RUN GREEN — wipes were dead weight. make ci GREEN 3399/0 @2:39 (+17s ≈ 133ms/clone, consistent) | | 9 | Global-state audit (t.Setenv/os.Setenv/logger/http-default → testkit injection); enable t.Parallel on proven-safe; connection budgets; `-race` clean; drop -p 1 | 3 ⛩ | S | pending | | wall-clock vs task-1 baseline recorded here | | 10 | Contract-manifest CI check (WantedCollections ↔ //coves:ingestion-contract markers) + T2 skeleton (serial runner via compose runner; make test-e2e; test-e2e-dev escape hatch) | 4 ⛩ | S | pending | | build BEFORE first contract so every contract lands against it | | 11 | Contracts: community (community.profile ingestion + API) — strangler: behavior inventory of community_e2e_test.go (1820 LOC) → down-tier T1s → contract → delete old | 4 | S | pending | | template for tasks 12-16; sync-indexing trap per spec §3.4 | @@ -187,3 +187,14 @@ Stop the loop when every task is done, or on any blocked task. shared-DB-vs-clone desync hazard is confined to tests/e2e; tasks 10-16 must keep it that way (T2 contracts observe via the REAL AppView's endpoints and never mix testkit.DB clones with container-side writes). +- **From task 8**: tests/ gets databases ONLY via testkit (grep-verified + empty). make test-integration's contract CHANGED: fails loudly without + the dev stack (Require floor) instead of skip-green — one-line revert + possible (drop RequirePDS/RequireJetstream) if a Postgres-only local tier + is ever wanted. Pre-existing dead code in oauth_helpers.go + (CreateTestUserOnPDS, VerifySessionData, GenerateTestSealSecret — zero + callers) left for tasks 10-16. purgeIdentityCache in tests/live KEPT: + guards a within-run invariant (Method != cache), not a cross-run wipe. + Cumulative clone cost at 3399 tests: ~35s over baseline (~133ms/clone) — + task 9's parallelism must beat that. No AppView-written row is asserted + from Go anywhere (clean T2 boundary for tasks 10-16). diff --git a/tests/e2e/error_recovery_test.go b/tests/e2e/error_recovery_test.go index 50f95db..7a3878e 100644 --- a/tests/e2e/error_recovery_test.go +++ b/tests/e2e/error_recovery_test.go @@ -7,19 +7,14 @@ import ( "Coves/internal/atproto/jetstream" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" - "database/sql" - "fmt" "net/http" "net/http/httptest" - "os" "strings" "sync/atomic" "testing" "time" - - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" ) // TestE2E_ErrorRecovery tests system resilience and recovery from various failures @@ -41,12 +36,7 @@ func TestE2E_ErrorRecovery(t *testing.T) { // NOTE: This tests connection retry logic, not actual reconnection after disconnect. // True reconnection testing would require: connect → send events → disconnect → reconnect → continue func testJetstreamReconnection(t *testing.T) { - db := setupErrorRecoveryTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) resolver := identity.NewResolver(db, identity.DefaultConfig()) @@ -136,12 +126,7 @@ func testJetstreamReconnection(t *testing.T) { // testMalformedJetstreamEvents verifies that malformed events are skipped gracefully // without crashing the consumer func testMalformedJetstreamEvents(t *testing.T) { - db := setupErrorRecoveryTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) resolver := identity.NewResolver(db, identity.DefaultConfig()) @@ -279,12 +264,7 @@ func testMalformedJetstreamEvents(t *testing.T) { // testDatabaseConnectionRecovery verifies graceful handling of database connection loss func testDatabaseConnectionRecovery(t *testing.T) { - db := setupErrorRecoveryTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) resolver := identity.NewResolver(db, identity.DefaultConfig()) @@ -354,12 +334,7 @@ func testDatabaseConnectionRecovery(t *testing.T) { // testPDSUnavailability verifies graceful degradation when PDS is temporarily unavailable func testPDSUnavailability(t *testing.T) { - db := setupErrorRecoveryTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) resolver := identity.NewResolver(db, identity.DefaultConfig()) @@ -481,12 +456,7 @@ func testPDSUnavailability(t *testing.T) { // testOutOfOrderEvents verifies that events arriving out of sequence are handled correctly func testOutOfOrderEvents(t *testing.T) { - db := setupErrorRecoveryTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) resolver := identity.NewResolver(db, identity.DefaultConfig()) @@ -617,59 +587,3 @@ func testOutOfOrderEvents(t *testing.T) { t.Log("✓ Duplicate events handled idempotently") }) } - -// setupErrorRecoveryTestDB sets up a clean test database for error recovery tests -func setupErrorRecoveryTestDB(t *testing.T) *sql.DB { - t.Helper() - - testUser := os.Getenv("POSTGRES_TEST_USER") - testPassword := os.Getenv("POSTGRES_TEST_PASSWORD") - testPort := os.Getenv("POSTGRES_TEST_PORT") - testDB := os.Getenv("POSTGRES_TEST_DB") - - if testUser == "" { - testUser = "test_user" - } - if testPassword == "" { - testPassword = "test_password" - } - if testPort == "" { - testPort = "5434" - } - if testDB == "" { - testDB = "coves_test" - } - - dbURL := fmt.Sprintf("postgres://%s:%s@localhost:%s/%s?sslmode=disable", - testUser, testPassword, testPort, testDB) - - db, err := sql.Open("postgres", dbURL) - if err != nil { - t.Fatalf("Failed to connect to test database: %v", err) - } - - if pingErr := db.Ping(); pingErr != nil { - t.Fatalf("Failed to ping test database: %v", pingErr) - } - - if dialectErr := goose.SetDialect("postgres"); dialectErr != nil { - t.Fatalf("Failed to set goose dialect: %v", dialectErr) - } - - if migrateErr := goose.Up(db, "../../internal/db/migrations"); migrateErr != nil { - t.Fatalf("Failed to run migrations: %v", migrateErr) - } - - // Clean up test data - be specific to avoid deleting unintended data - // Only delete known test handles from error recovery tests - _, _ = db.Exec(`DELETE FROM users WHERE handle IN ( - 'reconnect.test', - 'recovery.test', - 'pdsfail.test', - 'pdsrecovery.test', - 'malformed.test', - 'outoforder.test' - )`) - - return db -} diff --git a/tests/e2e/user_signup_test.go b/tests/e2e/user_signup_test.go index 5fbf3b0..9ff60a5 100644 --- a/tests/e2e/user_signup_test.go +++ b/tests/e2e/user_signup_test.go @@ -4,7 +4,6 @@ package e2e import ( "bytes" - "database/sql" "encoding/json" "fmt" "io" @@ -15,9 +14,6 @@ import ( "time" "Coves/tests/testkit" - - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" ) // TestMain controls test setup for the e2e package. @@ -330,57 +326,6 @@ func isAppViewAvailable(t *testing.T) bool { return resp.StatusCode == http.StatusOK } -// setupTestDB connects to test database and runs migrations -func setupTestDB(t *testing.T) *sql.DB { - // Build connection string from environment variables (set by .env.dev) - testUser := os.Getenv("POSTGRES_TEST_USER") - testPassword := os.Getenv("POSTGRES_TEST_PASSWORD") - testPort := os.Getenv("POSTGRES_TEST_PORT") - testDB := os.Getenv("POSTGRES_TEST_DB") - - // Fallback to defaults if not set - if testUser == "" { - testUser = "test_user" - } - if testPassword == "" { - testPassword = "test_password" - } - if testPort == "" { - testPort = "5434" - } - if testDB == "" { - testDB = "coves_test" - } - - dbURL := fmt.Sprintf("postgres://%s:%s@localhost:%s/%s?sslmode=disable", - testUser, testPassword, testPort, testDB) - - db, err := sql.Open("postgres", dbURL) - if err != nil { - t.Fatalf("Failed to connect to test database: %v", err) - } - - if pingErr := db.Ping(); pingErr != nil { - t.Fatalf("Failed to ping test database: %v", pingErr) - } - - if dialectErr := goose.SetDialect("postgres"); dialectErr != nil { - t.Fatalf("Failed to set goose dialect: %v", dialectErr) - } - - if migrateErr := goose.Up(db, "../../internal/db/migrations"); migrateErr != nil { - t.Fatalf("Failed to run migrations: %v", migrateErr) - } - - // Clean up any existing test data - _, err = db.Exec("DELETE FROM users WHERE handle LIKE '%.test' OR handle LIKE '%.local.coves.dev'") - if err != nil { - t.Logf("Warning: Failed to clean up test data: %v", err) - } - - return db -} - // getProfileViaAPI queries the AppView API to get a user profile by DID func getProfileViaAPI(did string) (string, string, error) { resp, err := http.Get(fmt.Sprintf("http://localhost:8081/xrpc/social.coves.actor.getProfile?actor=%s", did)) diff --git a/tests/integration/aggregator_e2e_test.go b/tests/integration/aggregator_e2e_test.go index 5d71240..af8d52f 100644 --- a/tests/integration/aggregator_e2e_test.go +++ b/tests/integration/aggregator_e2e_test.go @@ -23,7 +23,6 @@ import ( "testing" "time" - _ "github.com/lib/pq" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/tests/integration/author_posts_e2e_test.go b/tests/integration/author_posts_e2e_test.go index 81df57b..e885ab0 100644 --- a/tests/integration/author_posts_e2e_test.go +++ b/tests/integration/author_posts_e2e_test.go @@ -22,7 +22,6 @@ import ( "time" "github.com/go-chi/chi/v5" - _ "github.com/lib/pq" ) // getPostTitleFromView extracts title from PostView.Record. diff --git a/tests/integration/comment_e2e_test.go b/tests/integration/comment_e2e_test.go index 7fe75a2..72dbb8f 100644 --- a/tests/integration/comment_e2e_test.go +++ b/tests/integration/comment_e2e_test.go @@ -22,7 +22,6 @@ import ( oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/gorilla/websocket" - _ "github.com/lib/pq" ) // TestCommentE2E_CreateWithJetstream tests the full comment creation flow with real Jetstream diff --git a/tests/integration/comment_write_test.go b/tests/integration/comment_write_test.go index 450ec56..333c1fc 100644 --- a/tests/integration/comment_write_test.go +++ b/tests/integration/comment_write_test.go @@ -21,7 +21,6 @@ import ( oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" - _ "github.com/lib/pq" ) // TestCommentWrite_CreateTopLevelComment tests creating a comment on a post via E2E flow diff --git a/tests/integration/community_avatar_e2e_test.go b/tests/integration/community_avatar_e2e_test.go index 669ab8c..dbf356f 100644 --- a/tests/integration/community_avatar_e2e_test.go +++ b/tests/integration/community_avatar_e2e_test.go @@ -24,7 +24,6 @@ import ( "time" "github.com/gorilla/websocket" - _ "github.com/lib/pq" ) // createTestPNGImage creates a simple PNG image for testing diff --git a/tests/integration/community_e2e_test.go b/tests/integration/community_e2e_test.go index 4dce3b7..09558b9 100644 --- a/tests/integration/community_e2e_test.go +++ b/tests/integration/community_e2e_test.go @@ -28,7 +28,6 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" - _ "github.com/lib/pq" ) // TestCommunity_E2E is a TRUE end-to-end test covering the complete flow: diff --git a/tests/integration/community_suggestion_e2e_test.go b/tests/integration/community_suggestion_e2e_test.go index 7be16b5..05552e7 100644 --- a/tests/integration/community_suggestion_e2e_test.go +++ b/tests/integration/community_suggestion_e2e_test.go @@ -17,7 +17,6 @@ import ( "time" "github.com/go-chi/chi/v5" - _ "github.com/lib/pq" ) // --- Test helpers --- diff --git a/tests/integration/community_update_e2e_test.go b/tests/integration/community_update_e2e_test.go index 448013b..49f0ec8 100644 --- a/tests/integration/community_update_e2e_test.go +++ b/tests/integration/community_update_e2e_test.go @@ -21,7 +21,6 @@ import ( "time" "github.com/gorilla/websocket" - _ "github.com/lib/pq" ) // TestCommunityUpdateE2E_WithJetstream tests the FULL community update flow with REAL Jetstream diff --git a/tests/integration/discover_test.go b/tests/integration/discover_test.go index b6ab252..2b418ef 100644 --- a/tests/integration/discover_test.go +++ b/tests/integration/discover_test.go @@ -7,6 +7,7 @@ import ( "Coves/internal/api/middleware" "Coves/internal/core/votes" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "encoding/json" "fmt" @@ -72,8 +73,7 @@ func (m *mockVoteService) GetViewerVotesForSubjects(userDID string, subjectURIs // TestGetDiscover_ShowsAllCommunities tests discover feed shows posts from ALL communities func TestGetDiscover_ShowsAllCommunities(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret") @@ -139,8 +139,7 @@ func TestGetDiscover_ShowsAllCommunities(t *testing.T) { // TestGetDiscover_NoAuthRequired tests discover feed works without authentication func TestGetDiscover_NoAuthRequired(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret") @@ -182,8 +181,7 @@ func TestGetDiscover_NoAuthRequired(t *testing.T) { // TestGetDiscover_HotSort tests hot sorting across all communities func TestGetDiscover_HotSort(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret") @@ -235,8 +233,7 @@ func TestGetDiscover_HotSort(t *testing.T) { // a day-old genuinely popular post should outrank a six-hour-old post nobody // voted on. func TestGetDiscover_HotSort_LogDampedRanking(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret") @@ -291,8 +288,7 @@ func TestGetDiscover_HotSort_LogDampedRanking(t *testing.T) { // exactly once, in rank order, with no skips or duplicates. A divergence // between the live and cursor formulas fails this test. func TestGetDiscover_HotSort_PaginationCoversNegativeScores(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret") discoverService := discoverCore.NewDiscoverService(discoverRepo) @@ -311,8 +307,8 @@ func TestGetDiscover_HotSort_PaginationCoversNegativeScores(t *testing.T) { expectedOrder := []string{freshURI, blowoutURI, sixHourURI, downvotedURI} - // Paginate to exhaustion, one post per page (setupTestDB wiped the posts - // table, so the feed contains exactly these four posts) + // Paginate to exhaustion, one post per page (the database is a private + // clone, so the feed contains exactly these four posts) var seen []string cursor := "" for page := 0; page < 10; page++ { @@ -348,8 +344,7 @@ func TestGetDiscover_HotSort_PaginationCoversNegativeScores(t *testing.T) { // future-dated post ranks like a brand-new 0-vote post — it must not error // the query (negative POWER base) and must not outrank a post with real votes. func TestGetDiscover_HotSort_FutureDatedPost(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret") discoverService := discoverCore.NewDiscoverService(discoverRepo) @@ -390,8 +385,7 @@ func TestGetDiscover_HotSort_FutureDatedPost(t *testing.T) { // TestGetDiscover_Pagination tests cursor-based pagination func TestGetDiscover_Pagination(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret") @@ -443,8 +437,7 @@ func TestGetDiscover_Pagination(t *testing.T) { // TestGetDiscover_LimitValidation tests limit parameter validation func TestGetDiscover_LimitValidation(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret") @@ -469,8 +462,7 @@ func TestGetDiscover_LimitValidation(t *testing.T) { // TestGetDiscover_ViewerVoteState tests that authenticated users see their vote state on posts func TestGetDiscover_ViewerVoteState(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) ctx := context.Background() testID := time.Now().UnixNano() @@ -553,8 +545,7 @@ func TestGetDiscover_ViewerVoteState(t *testing.T) { // TestGetDiscover_NoViewerStateWithoutAuth tests that unauthenticated users don't get viewer state func TestGetDiscover_NoViewerStateWithoutAuth(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) ctx := context.Background() testID := time.Now().UnixNano() diff --git a/tests/integration/feed_test.go b/tests/integration/feed_test.go index c5a06be..21ff808 100644 --- a/tests/integration/feed_test.go +++ b/tests/integration/feed_test.go @@ -8,6 +8,7 @@ import ( "Coves/internal/core/communityFeeds" "Coves/internal/core/posts" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "encoding/json" "fmt" @@ -40,8 +41,7 @@ func getPostTitle(t *testing.T, pv *posts.PostView) string { // TestGetCommunityFeed_Hot tests hot feed sorting algorithm func TestGetCommunityFeed_Hot(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -115,8 +115,7 @@ func TestGetCommunityFeed_Hot(t *testing.T) { // TestGetCommunityFeed_Top_WithTimeframe tests top sorting with time filters func TestGetCommunityFeed_Top_WithTimeframe(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -189,8 +188,7 @@ func TestGetCommunityFeed_Top_WithTimeframe(t *testing.T) { // TestGetCommunityFeed_New tests chronological sorting func TestGetCommunityFeed_New(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -243,8 +241,7 @@ func TestGetCommunityFeed_New(t *testing.T) { // TestGetCommunityFeed_Pagination tests cursor-based pagination func TestGetCommunityFeed_Pagination(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -332,8 +329,7 @@ func TestGetCommunityFeed_Pagination(t *testing.T) { // TestGetCommunityFeed_InvalidCommunity tests error handling for invalid community func TestGetCommunityFeed_InvalidCommunity(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -366,8 +362,7 @@ func TestGetCommunityFeed_InvalidCommunity(t *testing.T) { // TestGetCommunityFeed_InvalidCursor tests cursor validation func TestGetCommunityFeed_InvalidCursor(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -420,8 +415,7 @@ func TestGetCommunityFeed_InvalidCursor(t *testing.T) { // TestGetCommunityFeed_EmptyFeed tests handling of empty communities func TestGetCommunityFeed_EmptyFeed(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -462,8 +456,7 @@ func TestGetCommunityFeed_EmptyFeed(t *testing.T) { // TestGetCommunityFeed_LimitValidation tests limit parameter validation func TestGetCommunityFeed_LimitValidation(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -513,8 +506,7 @@ func TestGetCommunityFeed_LimitValidation(t *testing.T) { // TestGetCommunityFeed_HotPaginationBug tests the critical hot pagination bug fix // Verifies that posts with higher raw scores but lower hot ranks don't get dropped during pagination func TestGetCommunityFeed_HotPaginationBug(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -612,8 +604,7 @@ func TestGetCommunityFeed_HotPaginationBug(t *testing.T) { // TestGetCommunityFeed_HotCursorPrecision tests that hot rank cursor preserves full float precision // Regression test for precision bug where posts with hot ranks differing by <1e-6 were dropped func TestGetCommunityFeed_HotCursorPrecision(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -712,8 +703,7 @@ func TestGetCommunityFeed_HotCursorPrecision(t *testing.T) { // Fix: Store the cursor creation timestamp in the cursor and use it for subsequent comparisons, // ensuring stable hot_rank computation across pagination requests. func TestGetCommunityFeed_HotCursorTimeDrift(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") @@ -811,8 +801,7 @@ func TestGetCommunityFeed_HotCursorTimeDrift(t *testing.T) { // TestGetCommunityFeed_BlobURLTransformation tests that blob refs are transformed to URLs func TestGetCommunityFeed_BlobURLTransformation(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") diff --git a/tests/integration/identity_resolution_test.go b/tests/integration/identity_resolution_test.go index 62d0fdf..bf75d55 100644 --- a/tests/integration/identity_resolution_test.go +++ b/tests/integration/identity_resolution_test.go @@ -4,6 +4,7 @@ package integration import ( "Coves/internal/atproto/identity" + "Coves/tests/testkit" "context" "fmt" "testing" @@ -17,12 +18,7 @@ func uniqueID() string { // TestIdentityCache tests the PostgreSQL identity cache operations func TestIdentityCache(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) cache := identity.NewPostgresCache(db, 5*time.Minute) ctx := context.Background() @@ -215,12 +211,7 @@ func TestIdentityCache(t *testing.T) { // TestIdentityCacheTTL tests that expired cache entries are not returned func TestIdentityCacheTTL(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Create cache with very short TTL (reduced from 1s to 100ms for faster, less flaky tests) ttl := 100 * time.Millisecond @@ -261,12 +252,7 @@ func TestIdentityCacheTTL(t *testing.T) { // TestIdentityResolverWithCache tests the caching resolver behavior func TestIdentityResolverWithCache(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) cache := identity.NewPostgresCache(db, 5*time.Minute) diff --git a/tests/integration/image_proxy_e2e_test.go b/tests/integration/image_proxy_e2e_test.go index 25901ea..69461dd 100644 --- a/tests/integration/image_proxy_e2e_test.go +++ b/tests/integration/image_proxy_e2e_test.go @@ -29,6 +29,7 @@ import ( "Coves/internal/core/communities" imageproxycore "Coves/internal/core/imageproxy" "Coves/internal/db/postgres" + "Coves/tests/testkit" ) // TestImageProxy_E2E tests the complete image proxy flow including: @@ -47,8 +48,7 @@ func TestImageProxy_E2E(t *testing.T) { _ = healthResp.Body.Close() // Setup test database - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() @@ -378,8 +378,7 @@ func TestImageProxy_CacheHit(t *testing.T) { _ = healthResp.Body.Close() // Setup test database - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() diff --git a/tests/integration/jetstream_consumer_test.go b/tests/integration/jetstream_consumer_test.go index d65719e..d68cdd2 100644 --- a/tests/integration/jetstream_consumer_test.go +++ b/tests/integration/jetstream_consumer_test.go @@ -7,18 +7,14 @@ import ( "Coves/internal/atproto/jetstream" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "testing" "time" ) func TestUserIndexingFromJetstream(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Wire up dependencies userRepo := postgres.NewUserRepository(db) @@ -311,12 +307,7 @@ func TestUserIndexingFromJetstream(t *testing.T) { } func TestUserServiceIdempotency(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) resolver := identity.NewResolver(db, identity.DefaultConfig()) diff --git a/tests/integration/oauth_e2e_test.go b/tests/integration/oauth_e2e_test.go index 2c6b9db..3b666c2 100644 --- a/tests/integration/oauth_e2e_test.go +++ b/tests/integration/oauth_e2e_test.go @@ -4,6 +4,7 @@ package integration import ( "Coves/internal/atproto/oauth" + "Coves/tests/testkit" "context" "encoding/json" "fmt" @@ -16,8 +17,6 @@ import ( oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,16 +33,7 @@ import ( // (HTTPS required for authorization servers and redirect URIs). func TestOAuth_Components(t *testing.T) { // Setup test database - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() - - // Run migrations to ensure OAuth tables exist - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) t.Log("🔧 Testing OAuth Components") @@ -145,12 +135,7 @@ func testOAuthComponentsWithMockedSession(t *testing.T, ctx context.Context, _ i // TestOAuthE2E_TokenExpiration tests that expired sealed tokens are rejected func TestOAuthE2E_TokenExpiration(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) ctx := context.Background() @@ -207,12 +192,7 @@ func TestOAuthE2E_TokenExpiration(t *testing.T) { // TestOAuthE2E_InvalidToken tests that invalid/tampered tokens are rejected func TestOAuthE2E_InvalidToken(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) t.Log("🔒 Testing OAuth invalid token rejection...") @@ -268,12 +248,7 @@ func TestOAuthE2E_InvalidToken(t *testing.T) { // TestOAuthE2E_SessionNotFound tests behavior when session doesn't exist in DB func TestOAuthE2E_SessionNotFound(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) ctx := context.Background() @@ -300,12 +275,7 @@ func TestOAuthE2E_SessionNotFound(t *testing.T) { // TestOAuthE2E_MultipleSessionsPerUser tests that a user can have multiple active sessions func TestOAuthE2E_MultipleSessionsPerUser(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) ctx := context.Background() @@ -385,12 +355,7 @@ func TestOAuthE2E_MultipleSessionsPerUser(t *testing.T) { // TestOAuthE2E_AuthRequestStorage tests OAuth auth request storage and retrieval func TestOAuthE2E_AuthRequestStorage(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) ctx := context.Background() @@ -491,12 +456,7 @@ func TestOAuthE2E_AuthRequestStorage(t *testing.T) { // TestOAuthE2E_TokenRefresh tests the refresh token flow func TestOAuthE2E_TokenRefresh(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) ctx := context.Background() @@ -743,12 +703,7 @@ func TestOAuthE2E_TokenRefresh(t *testing.T) { // TestOAuthE2E_SessionUpdate tests that refresh updates the session in database func TestOAuthE2E_SessionUpdate(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) ctx := context.Background() @@ -826,12 +781,7 @@ func TestOAuthE2E_SessionUpdate(t *testing.T) { // TestOAuthE2E_RefreshTokenRotation tests refresh token rotation behavior func TestOAuthE2E_RefreshTokenRotation(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) ctx := context.Background() diff --git a/tests/integration/oauth_helpers.go b/tests/integration/oauth_helpers.go index ec1d1db..69bc996 100644 --- a/tests/integration/oauth_helpers.go +++ b/tests/integration/oauth_helpers.go @@ -114,25 +114,6 @@ func SetupOAuthTestStore(t *testing.T, db *sql.DB) oauthlib.ClientAuthStore { return wrappedStore } -// CleanupOAuthTestData removes OAuth test data from the database -func CleanupOAuthTestData(t *testing.T, db *sql.DB, did string) { - t.Helper() - - ctx := context.Background() - - // Delete sessions for this DID - _, err := db.ExecContext(ctx, "DELETE FROM oauth_sessions WHERE did = $1", did) - if err != nil { - t.Logf("Warning: Failed to cleanup OAuth sessions: %v", err) - } - - // Delete auth requests (cleanup all expired ones) - _, err = db.ExecContext(ctx, "DELETE FROM oauth_requests WHERE created_at < NOW() - INTERVAL '1 hour'") - if err != nil { - t.Logf("Warning: Failed to cleanup OAuth auth requests: %v", err) - } -} - // VerifySessionData verifies that session data is properly stored and retrievable func VerifySessionData(t *testing.T, store oauthlib.ClientAuthStore, did syntax.DID, sessionID string) { t.Helper() diff --git a/tests/integration/oauth_session_fixation_test.go b/tests/integration/oauth_session_fixation_test.go index 4624128..f44a0ad 100644 --- a/tests/integration/oauth_session_fixation_test.go +++ b/tests/integration/oauth_session_fixation_test.go @@ -4,6 +4,7 @@ package integration import ( "Coves/internal/atproto/oauth" + "Coves/tests/testkit" "context" "crypto/sha256" "encoding/base64" @@ -17,7 +18,6 @@ import ( oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" - "github.com/pressly/goose/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -35,16 +35,7 @@ import ( // 6. WITH THE FIX: Binding mismatch is detected, mobile cookies cleared, user gets web session func TestOAuth_SessionFixationAttackPrevention(t *testing.T) { // Setup test database - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) // Setup OAuth client and store store := SetupOAuthTestStore(t, db) diff --git a/tests/integration/oauth_session_handle_sync_test.go b/tests/integration/oauth_session_handle_sync_test.go index 750c607..9c53cc3 100644 --- a/tests/integration/oauth_session_handle_sync_test.go +++ b/tests/integration/oauth_session_handle_sync_test.go @@ -18,6 +18,7 @@ import ( "Coves/internal/atproto/oauth" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" ) // TestOAuthSessionHandleSync tests that OAuth session handles are updated @@ -35,12 +36,7 @@ import ( // TEST_DATABASE_URL="postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" \ // go test -v ./tests/integration/ -run "TestOAuthSessionHandleSync" func TestOAuthSessionHandleSync(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -303,12 +299,7 @@ func TestOAuthSessionHandleSync_LiveJetstream(t *testing.T) { t.Skip("PDS not available at localhost:3001 - run 'docker-compose up -d pds' first") } - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() diff --git a/tests/integration/post_consumer_test.go b/tests/integration/post_consumer_test.go index 6aff2f9..4286b5b 100644 --- a/tests/integration/post_consumer_test.go +++ b/tests/integration/post_consumer_test.go @@ -6,6 +6,7 @@ import ( "Coves/internal/atproto/jetstream" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "fmt" "testing" @@ -19,12 +20,7 @@ import ( // comment suggests reconciliation is not implemented. This test verifies that // the reconciliation logic in post_consumer.go:210-226 works correctly. func TestPostConsumer_CommentCountReconciliation(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() diff --git a/tests/integration/post_creation_test.go b/tests/integration/post_creation_test.go index 2a1c08c..aa221df 100644 --- a/tests/integration/post_creation_test.go +++ b/tests/integration/post_creation_test.go @@ -9,6 +9,7 @@ import ( "Coves/internal/core/posts" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "fmt" "strings" @@ -19,12 +20,7 @@ import ( ) func TestPostCreation_Basic(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Setup: Initialize services userRepo := postgres.NewUserRepository(db) @@ -48,11 +44,6 @@ func TestPostCreation_Basic(t *testing.T) { ctx := context.Background() - // Cleanup: Remove any existing test data - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:test%'") - _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:test%'") - _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:test%'") - // Setup: Create test user testUserDID := generateTestDID("postauthor") testUserHandle := "postauthor.test" @@ -289,17 +280,7 @@ func TestPostCreation_Basic(t *testing.T) { // TestPostRepository_Create tests the repository layer func TestPostRepository_Create(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() - - // Cleanup first - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:test%'") - _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:test%'") - _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:test%'") + db := testkit.DB(t) // Setup: Create test user and community ctx := context.Background() diff --git a/tests/integration/post_delete_test.go b/tests/integration/post_delete_test.go index bafa4c5..42c1a82 100644 --- a/tests/integration/post_delete_test.go +++ b/tests/integration/post_delete_test.go @@ -10,8 +10,8 @@ import ( "Coves/internal/core/posts" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" - "database/sql" "errors" "fmt" "net" @@ -22,8 +22,6 @@ import ( "time" "github.com/gorilla/websocket" - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" @@ -32,20 +30,10 @@ import ( // TestPostDeletion_JetstreamConsumer tests that the Jetstream consumer // correctly handles post deletion events by soft-deleting posts in the AppView database. func TestPostDeletion_JetstreamConsumer(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() - // Cleanup old test data - _, _ = db.Exec("DELETE FROM posts WHERE community_did = 'did:plc:deltest123'") - _, _ = db.Exec("DELETE FROM communities WHERE did = 'did:plc:deltest123'") - _, _ = db.Exec("DELETE FROM users WHERE did = 'did:plc:delauthor123'") - // Setup repositories userRepo := postgres.NewUserRepository(db) communityRepo := postgres.NewCommunityRepository(db) @@ -227,8 +215,7 @@ func TestPostDeletion_JetstreamConsumer(t *testing.T) { // TestPostDeletion_Authorization tests that only the post author can delete their posts func TestPostDeletion_Authorization(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() pdsURL := getTestPDSURL() @@ -345,8 +332,7 @@ func TestPostDeletion_Authorization(t *testing.T) { // TestPostDeletion_ServiceAuthorization tests the author verification logic in the service layer // This test requires a live PDS to fully test the authorization flow func TestPostDeletion_ServiceAuthorization_LivePDS(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() pdsURL := getTestPDSURL() @@ -487,24 +473,7 @@ func TestPostDeletion_ServiceAuthorization_LivePDS(t *testing.T) { // 5. Receive delete event from Jetstream // 6. Verify post is soft-deleted in AppView DB func TestPostE2E_DeleteWithJetstream(t *testing.T) { - // Setup test database - dbURL := os.Getenv("TEST_DATABASE_URL") - if dbURL == "" { - dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" - } - - db, err := sql.Open("postgres", dbURL) - if err != nil { - t.Fatalf("Failed to connect to test database: %v", err) - } - defer func() { _ = db.Close() }() - - if dialectErr := goose.SetDialect("postgres"); dialectErr != nil { - t.Fatalf("Failed to set goose dialect: %v", dialectErr) - } - if migrateErr := goose.Up(db, "../../internal/db/migrations"); migrateErr != nil { - t.Fatalf("Failed to run migrations: %v", migrateErr) - } + db := testkit.DB(t) pdsURL := getTestPDSURL() healthResp, err := http.Get(pdsURL + "/xrpc/_health") diff --git a/tests/integration/post_e2e_test.go b/tests/integration/post_e2e_test.go index f8a6142..614285f 100644 --- a/tests/integration/post_e2e_test.go +++ b/tests/integration/post_e2e_test.go @@ -10,9 +10,9 @@ import ( "Coves/internal/core/posts" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "bytes" "context" - "database/sql" "encoding/json" "fmt" "net" @@ -24,8 +24,6 @@ import ( "time" "github.com/gorilla/websocket" - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -44,17 +42,7 @@ import ( // NOTE: This test simulates the Jetstream event (step 4-5) since we don't have // a live PDS/Jetstream in test environment. For true live testing, use TestPostCreation_E2E_LivePDS. func TestPostCreation_E2E_WithJetstream(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() - - // Cleanup old test data first - _, _ = db.Exec("DELETE FROM posts WHERE community_did = 'did:plc:gaming123'") - _, _ = db.Exec("DELETE FROM communities WHERE did = 'did:plc:gaming123'") - _, _ = db.Exec("DELETE FROM users WHERE did = 'did:plc:alice123'") + db := testkit.DB(t) // Setup repositories userRepo := postgres.NewUserRepository(db) @@ -325,23 +313,7 @@ func TestPostCreation_E2E_WithJetstream(t *testing.T) { // - Live Jetstream running at the local dev Jetstream (JETSTREAM_FEEDS default: self=ws://localhost:6008) // - Test database running func TestPostCreation_E2E_LivePDS(t *testing.T) { - // Setup test database - dbURL := os.Getenv("TEST_DATABASE_URL") - if dbURL == "" { - dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" - } - - db, err := sql.Open("postgres", dbURL) - require.NoError(t, err, "Failed to connect to test database") - defer func() { - if closeErr := db.Close(); closeErr != nil { - t.Logf("Failed to close database: %v", closeErr) - } - }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) // Check if PDS is running pdsURL := os.Getenv("PDS_URL") @@ -412,11 +384,6 @@ func TestPostCreation_E2E_LivePDS(t *testing.T) { ctx := context.Background() - // Cleanup old test data - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:e2etest%'") - _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:e2etest%'") - _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:e2etest%'") - // Create test user (author) author := createTestUser(t, db, "e2etestauthor.bsky.social", "did:plc:e2etestauthor123") diff --git a/tests/integration/post_handler_test.go b/tests/integration/post_handler_test.go index f151809..f7cf5c5 100644 --- a/tests/integration/post_handler_test.go +++ b/tests/integration/post_handler_test.go @@ -8,6 +8,7 @@ import ( "Coves/internal/core/communities" "Coves/internal/core/posts" "Coves/internal/db/postgres" + "Coves/tests/testkit" "bytes" "encoding/json" "net/http" @@ -21,12 +22,7 @@ import ( // TestPostHandler_SecurityValidation tests HTTP handler-level security checks func TestPostHandler_SecurityValidation(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Setup services communityRepo := postgres.NewCommunityRepository(db) @@ -387,12 +383,7 @@ func TestPostHandler_SecurityValidation(t *testing.T) { // TestPostHandler_SpecialCharacters tests content with special characters func TestPostHandler_SpecialCharacters(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Setup services communityRepo := postgres.NewCommunityRepository(db) @@ -469,12 +460,7 @@ func TestPostHandler_SpecialCharacters(t *testing.T) { // TestPostService_DIDValidationSecurity tests service-layer DID validation (defense-in-depth) func TestPostService_DIDValidationSecurity(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Setup services communityRepo := postgres.NewCommunityRepository(db) diff --git a/tests/integration/post_thumb_validation_test.go b/tests/integration/post_thumb_validation_test.go index 9eccaff..51ec3f2 100644 --- a/tests/integration/post_thumb_validation_test.go +++ b/tests/integration/post_thumb_validation_test.go @@ -8,6 +8,7 @@ import ( "Coves/internal/core/communities" "Coves/internal/core/posts" "Coves/internal/db/postgres" + "Coves/tests/testkit" "bytes" "context" "encoding/json" @@ -44,12 +45,7 @@ func createTestCommunityWithCredentials(t *testing.T, repo communities.Repositor // TestPostHandler_ThumbValidation tests strict validation of thumb field in external embeds func TestPostHandler_ThumbValidation(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Setup services communityRepo := postgres.NewCommunityRepository(db) @@ -69,16 +65,7 @@ func TestPostHandler_ThumbValidation(t *testing.T) { handler := post.NewCreateHandler(postService) - // The test helpers INSERT without upsert and test DIDs are stable across - // runs, so clean up any rows left by a previous run before recreating them. - // (Without this, running this test in isolation against the persistent test - // DB collides on users_pkey — the full suite only avoids it by collateral - // deletes in earlier-sorted tests.) userDID := "did:plc:thumbtest" + t.Name() - communityDID := "did:plc:testcommunity" + t.Name() - _, _ = db.Exec("DELETE FROM posts WHERE community_did = $1", communityDID) - _, _ = db.Exec("DELETE FROM communities WHERE did = $1", communityDID) - _, _ = db.Exec("DELETE FROM users WHERE did = $1", userDID) // Create test user and community with PDS credentials (use unique IDs) testUser := createTestUser(t, db, "thumbtest.bsky.social", userDID) @@ -305,12 +292,7 @@ func TestPostHandler_ThumbValidation(t *testing.T) { // tests still pass — guarding against regression of the silent-corruption bug // the validation exists to prevent. func TestPostHandler_EmbedValidation(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) communityRepo := postgres.NewCommunityRepository(db) communityService := communities.NewCommunityServiceWithPDSFactory( @@ -326,13 +308,7 @@ func TestPostHandler_EmbedValidation(t *testing.T) { postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, "http://localhost:3001") handler := post.NewCreateHandler(postService) - // The test helpers INSERT without upsert and test DIDs are stable across - // runs, so clean up any rows left by a previous run before recreating them. userDID := "did:plc:embedtest" + t.Name() - communityDID := "did:plc:testcommunity" + t.Name() - _, _ = db.Exec("DELETE FROM posts WHERE community_did = $1", communityDID) - _, _ = db.Exec("DELETE FROM communities WHERE did = $1", communityDID) - _, _ = db.Exec("DELETE FROM users WHERE did = $1", userDID) testUser := createTestUser(t, db, "embedtest.bsky.social", userDID) testCommunity := createTestCommunityWithCredentials(t, communityRepo, t.Name()) diff --git a/tests/integration/post_unfurl_test.go b/tests/integration/post_unfurl_test.go index f787310..b2d669b 100644 --- a/tests/integration/post_unfurl_test.go +++ b/tests/integration/post_unfurl_test.go @@ -11,6 +11,7 @@ import ( "Coves/internal/core/unfurl" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "encoding/json" "fmt" @@ -26,12 +27,7 @@ import ( // TestPostUnfurl_UnsupportedURL tests that posts with unsupported URLs still succeed func TestPostUnfurl_UnsupportedURL(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -65,11 +61,6 @@ func TestPostUnfurl_UnsupportedURL(t *testing.T) { "http://localhost:3001", ) - // Cleanup - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:unsupported%'") - _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:unsupported%'") - _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:unsupported%'") - // Create test user testUserDID := generateTestDID("unsupporteduser") _, err := userService.CreateUser(ctx, users.CreateUserRequest{ @@ -126,12 +117,7 @@ func TestPostUnfurl_UnsupportedURL(t *testing.T) { // TestPostUnfurl_MissingEmbedType tests posts without external embed type don't trigger unfurling func TestPostUnfurl_MissingEmbedType(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -169,11 +155,6 @@ func TestPostUnfurl_MissingEmbedType(t *testing.T) { "http://localhost:3001", ) - // Cleanup - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:noembed%'") - _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:noembed%'") - _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:noembed%'") - // Create test user and community testUserDID := generateTestDID("noembeduser") _, err := userService.CreateUser(ctx, users.CreateUserRequest{ @@ -261,12 +242,7 @@ func TestPostUnfurl_MissingEmbedType(t *testing.T) { // The kagi-news trusted aggregator already supplies authoritative metadata from // the Kagi JSON feed, so the unfurl path for Kite URLs is intentionally disabled. func TestPostUnfurl_KagiKiteExcluded(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -302,12 +278,7 @@ func TestPostUnfurl_KagiKiteExcluded(t *testing.T) { // TestPostUnfurl_E2E_WithJetstream tests the full unfurl flow with Jetstream consumer // This simulates: Create post → unfurl → write to PDS → Jetstream event → index in AppView func TestPostUnfurl_E2E_WithJetstream(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -343,12 +314,6 @@ func TestPostUnfurl_E2E_WithJetstream(t *testing.T) { defer unfurlTarget.Close() targetURL := unfurlTarget.URL + "/e2etest" - // Cleanup - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:e2eunfurl%'") - _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:e2eunfurl%'") - _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:e2eunfurl%'") - _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url = $1", targetURL) - // Create test data testUserDID := generateTestDID("e2eunfurluser") author := createTestUser(t, db, "e2eunfurluser.test", testUserDID) diff --git a/tests/integration/subscription_indexing_test.go b/tests/integration/subscription_indexing_test.go index 211e316..c808820 100644 --- a/tests/integration/subscription_indexing_test.go +++ b/tests/integration/subscription_indexing_test.go @@ -5,6 +5,7 @@ package integration import ( "Coves/internal/atproto/jetstream" "Coves/internal/core/communities" + "Coves/tests/testkit" "context" "database/sql" "fmt" @@ -18,8 +19,7 @@ import ( // from Jetstream events and stored in the AppView database func TestSubscriptionIndexing_ContentVisibility(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupTestDB(t, db) + db := testkit.DB(t) repo := createTestCommunityRepo(t, db) // Skip verification in tests @@ -239,8 +239,7 @@ func TestSubscriptionIndexing_ContentVisibility(t *testing.T) { // TestSubscriptionIndexing_DeleteOperations tests unsubscribe (DELETE) event handling func TestSubscriptionIndexing_DeleteOperations(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupTestDB(t, db) + db := testkit.DB(t) repo := createTestCommunityRepo(t, db) // Skip verification in tests @@ -351,8 +350,7 @@ func TestSubscriptionIndexing_DeleteOperations(t *testing.T) { // TestSubscriptionIndexing_SubscriberCount tests that subscriber counts are updated atomically func TestSubscriptionIndexing_SubscriberCount(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupTestDB(t, db) + db := testkit.DB(t) repo := createTestCommunityRepo(t, db) // Skip verification in tests @@ -486,11 +484,3 @@ func createTestCommunityRepo(t *testing.T, db interface{}) communities.Repositor // Import the postgres package to create a repo return postgresRepo.NewCommunityRepository(db.(*sql.DB)) } - -func cleanupTestDB(t *testing.T, db interface{}) { - t.Helper() - sqlDB := db.(*sql.DB) - if err := sqlDB.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } -} diff --git a/tests/integration/timeline_test.go b/tests/integration/timeline_test.go index ced0d7f..3f502b4 100644 --- a/tests/integration/timeline_test.go +++ b/tests/integration/timeline_test.go @@ -6,6 +6,7 @@ import ( "Coves/internal/api/handlers/timeline" "Coves/internal/api/middleware" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "encoding/json" "fmt" @@ -22,8 +23,7 @@ import ( // TestGetTimeline_Basic tests timeline feed shows posts from subscribed communities func TestGetTimeline_Basic(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services timelineRepo := postgres.NewTimelineRepository(db, "test-cursor-secret") @@ -107,8 +107,7 @@ func TestGetTimeline_Basic(t *testing.T) { // TestGetTimeline_HotSort tests hot sorting across multiple communities func TestGetTimeline_HotSort(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services timelineRepo := postgres.NewTimelineRepository(db, "test-cursor-secret") @@ -174,8 +173,7 @@ func TestGetTimeline_HotSort(t *testing.T) { // TestGetTimeline_Pagination tests cursor-based pagination func TestGetTimeline_Pagination(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services timelineRepo := postgres.NewTimelineRepository(db, "test-cursor-secret") @@ -246,8 +244,7 @@ func TestGetTimeline_Pagination(t *testing.T) { // TestGetTimeline_EmptyWhenNoSubscriptions tests timeline is empty when user has no subscriptions func TestGetTimeline_EmptyWhenNoSubscriptions(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services timelineRepo := postgres.NewTimelineRepository(db, "test-cursor-secret") @@ -284,8 +281,7 @@ func TestGetTimeline_EmptyWhenNoSubscriptions(t *testing.T) { // TestGetTimeline_Unauthorized tests timeline requires authentication func TestGetTimeline_Unauthorized(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services timelineRepo := postgres.NewTimelineRepository(db, "test-cursor-secret") @@ -309,8 +305,7 @@ func TestGetTimeline_Unauthorized(t *testing.T) { // TestGetTimeline_LimitValidation tests limit parameter validation func TestGetTimeline_LimitValidation(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services timelineRepo := postgres.NewTimelineRepository(db, "test-cursor-secret") @@ -356,8 +351,7 @@ func TestGetTimeline_LimitValidation(t *testing.T) { // - Tests all sorting modes (hot, top, new) across communities // - Ensures proper aggregation and no cross-contamination func TestGetTimeline_MultiCommunity_E2E(t *testing.T) { - db := setupTestDB(t) - t.Cleanup(func() { _ = db.Close() }) + db := testkit.DB(t) // Setup services timelineRepo := postgres.NewTimelineRepository(db, "test-cursor-secret") diff --git a/tests/integration/token_refresh_test.go b/tests/integration/token_refresh_test.go index 44e1ffa..e4db735 100644 --- a/tests/integration/token_refresh_test.go +++ b/tests/integration/token_refresh_test.go @@ -5,6 +5,7 @@ package integration import ( "Coves/internal/core/communities" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "encoding/base64" "encoding/json" @@ -96,12 +97,7 @@ func TestTokenRefresh_ExpirationDetection(t *testing.T) { // TestTokenRefresh_UpdateCredentials tests the repository UpdateCredentials method func TestTokenRefresh_UpdateCredentials(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) repo := postgres.NewCommunityRepository(db) @@ -162,12 +158,7 @@ func TestTokenRefresh_UpdateCredentials(t *testing.T) { // TestTokenRefresh_E2E_UpdateAfterTokenRefresh tests end-to-end token refresh during community update func TestTokenRefresh_E2E_UpdateAfterTokenRefresh(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // This test requires a real PDS for token refresh // For now, we'll test the token expiration detection logic diff --git a/tests/integration/user_journey_e2e_test.go b/tests/integration/user_journey_e2e_test.go index d3789c2..d49ea0c 100644 --- a/tests/integration/user_journey_e2e_test.go +++ b/tests/integration/user_journey_e2e_test.go @@ -10,6 +10,7 @@ import ( "Coves/internal/core/posts" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "bytes" "context" "database/sql" @@ -28,8 +29,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -53,23 +52,7 @@ import ( // - Timeline aggregation and feed generation func TestFullUserJourney_E2E(t *testing.T) { - // Setup test database - dbURL := os.Getenv("TEST_DATABASE_URL") - if dbURL == "" { - dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" - } - - db, err := sql.Open("postgres", dbURL) - require.NoError(t, err, "Failed to connect to test database") - defer func() { - if closeErr := db.Close(); closeErr != nil { - t.Logf("Failed to close database: %v", closeErr) - } - }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) // Check if PDS is running pdsURL := os.Getenv("PDS_URL") @@ -152,31 +135,11 @@ func TestFullUserJourney_E2E(t *testing.T) { httpServer := httptest.NewServer(r) defer httpServer.Close() - // Cleanup test data from previous runs (clean up ALL journey test data). - // A single collision-free testID is shared by every handle/community name and the - // deferred cleanup patterns below (uniqueTestID stays short for PDS handle limits). + // A single collision-free testID is shared by every handle and community + // name. The database is a private clone, but the PDS is not: accounts + // persist across runs, so handles still have to be unique (uniqueTestID + // stays short for the PDS handle-length limit). testID := uniqueTestID() - // Clean up previous test runs - use pattern that matches journey test data - // Handles are now shorter: alice{4-digit}.local.coves.dev, bob{4-digit}.local.coves.dev - _, _ = db.Exec("DELETE FROM votes WHERE voter_did LIKE '%alice%.local.coves.dev%' OR voter_did LIKE '%bob%.local.coves.dev%'") - _, _ = db.Exec("DELETE FROM comments WHERE commenter_did LIKE '%alice%.local.coves.dev%' OR commenter_did LIKE '%bob%.local.coves.dev%'") - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE '%gj%'") - _, _ = db.Exec("DELETE FROM community_subscriptions WHERE user_did LIKE '%alice%.local.coves.dev%' OR user_did LIKE '%bob%.local.coves.dev%'") - _, _ = db.Exec("DELETE FROM communities WHERE handle LIKE 'gj%'") - _, _ = db.Exec("DELETE FROM users WHERE handle LIKE 'alice%.local.coves.dev' OR handle LIKE 'bob%.local.coves.dev'") - - // Defer cleanup for current test run using specific timestamp pattern - defer func() { - alicePattern := fmt.Sprintf("%%alice%s%%", testID) - bobPattern := fmt.Sprintf("%%bob%s%%", testID) - gjPattern := fmt.Sprintf("%%gj%s%%", testID) - _, _ = db.Exec("DELETE FROM votes WHERE voter_did LIKE $1 OR voter_did LIKE $2", alicePattern, bobPattern) - _, _ = db.Exec("DELETE FROM comments WHERE commenter_did LIKE $1 OR commenter_did LIKE $2", alicePattern, bobPattern) - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE $1", gjPattern) - _, _ = db.Exec("DELETE FROM community_subscriptions WHERE user_did LIKE $1 OR user_did LIKE $2", alicePattern, bobPattern) - _, _ = db.Exec("DELETE FROM communities WHERE handle LIKE $1", gjPattern) - _, _ = db.Exec("DELETE FROM users WHERE handle LIKE $1 OR handle LIKE $2", alicePattern, bobPattern) - }() // Test variables to track state across steps var ( diff --git a/tests/integration/user_profile_avatar_e2e_test.go b/tests/integration/user_profile_avatar_e2e_test.go index 1aeec0d..3520fa4 100644 --- a/tests/integration/user_profile_avatar_e2e_test.go +++ b/tests/integration/user_profile_avatar_e2e_test.go @@ -9,9 +9,9 @@ import ( "Coves/internal/atproto/jetstream" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "bytes" "context" - "database/sql" "encoding/json" "errors" "fmt" @@ -29,8 +29,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -60,19 +58,7 @@ func createTestAvatarPNG(width, height int, c color.Color) []byte { // 3. Jetstream consumer receives and processes the event // 4. GetProfile returns the correct avatar URL func TestUserProfileAvatarE2E_UpdateWithAvatar(t *testing.T) { - // Setup test database - dbURL := os.Getenv("TEST_DATABASE_URL") - if dbURL == "" { - dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" - } - - db, err := sql.Open("postgres", dbURL) - require.NoError(t, err, "Failed to connect to test database") - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) // Check if PDS is running pdsURL := os.Getenv("PDS_URL") @@ -128,7 +114,6 @@ func TestUserProfileAvatarE2E_UpdateWithAvatar(t *testing.T) { // Cleanup old test data testID := uniqueTestID() - _, _ = db.Exec("DELETE FROM users WHERE handle LIKE 'avatartest%.local.coves.dev'") t.Run("update profile with avatar via real PDS and Jetstream", func(t *testing.T) { // Create test user account on PDS @@ -354,19 +339,7 @@ func TestUserProfileAvatarE2E_UpdateWithAvatar(t *testing.T) { // TestUserProfileAvatarE2E_UpdateWithBanner tests the full flow of updating a user profile with a banner func TestUserProfileAvatarE2E_UpdateWithBanner(t *testing.T) { - // Setup test database - dbURL := os.Getenv("TEST_DATABASE_URL") - if dbURL == "" { - dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" - } - - db, err := sql.Open("postgres", dbURL) - require.NoError(t, err, "Failed to connect to test database") - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) // Check if PDS is running pdsURL := os.Getenv("PDS_URL") @@ -417,7 +390,6 @@ func TestUserProfileAvatarE2E_UpdateWithBanner(t *testing.T) { defer httpServer.Close() testID := uniqueTestID() - _, _ = db.Exec("DELETE FROM users WHERE handle LIKE 'bannertest%.local.coves.dev'") t.Run("update profile with banner via real PDS and Jetstream", func(t *testing.T) { // Create test user account on PDS @@ -594,19 +566,7 @@ func TestUserProfileAvatarE2E_UpdateWithBanner(t *testing.T) { // TestUserProfileAvatarE2E_UpdateDisplayNameAndBio tests updating non-blob profile fields func TestUserProfileAvatarE2E_UpdateDisplayNameAndBio(t *testing.T) { - // Setup test database - dbURL := os.Getenv("TEST_DATABASE_URL") - if dbURL == "" { - dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" - } - - db, err := sql.Open("postgres", dbURL) - require.NoError(t, err, "Failed to connect to test database") - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) // Check if PDS is running pdsURL := os.Getenv("PDS_URL") @@ -794,19 +754,7 @@ func TestUserProfileAvatarE2E_UpdateDisplayNameAndBio(t *testing.T) { // TestUserProfileAvatarE2E_ReplaceAvatar tests replacing an existing avatar with a new one func TestUserProfileAvatarE2E_ReplaceAvatar(t *testing.T) { - // Setup test database - dbURL := os.Getenv("TEST_DATABASE_URL") - if dbURL == "" { - dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" - } - - db, err := sql.Open("postgres", dbURL) - require.NoError(t, err, "Failed to connect to test database") - defer func() { _ = db.Close() }() - - // Run migrations - require.NoError(t, goose.SetDialect("postgres")) - require.NoError(t, goose.Up(db, "../../internal/db/migrations")) + db := testkit.DB(t) // Check if PDS is running pdsURL := os.Getenv("PDS_URL") diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index 3e757e2..195e1ec 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -8,13 +8,11 @@ import ( "Coves/internal/atproto/pds" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" - "database/sql" "encoding/json" "errors" "fmt" - "io" - "log" "net/http" "net/http/httptest" "os" @@ -24,8 +22,6 @@ import ( "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/go-chi/chi/v5" - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" ) // testUserRouteOptions returns route options with a dummy PDS client factory. @@ -38,90 +34,12 @@ func testUserRouteOptions() *routes.UserRouteOptions { } } -// TestMain controls test setup for the integration package. -// Set LOG_ENABLED=false to suppress application log output during tests. +// TestMain controls test setup for the integration package. The whole package +// is integration-tagged, so its floor is the infrastructure every file here +// assumes: Postgres for testkit.DB clones, the PDS for record writes, and +// Jetstream for the consumer tests that read them back. func TestMain(m *testing.M) { - // Silence logs when LOG_ENABLED=false (what .env.ci sets for the gate) - if os.Getenv("LOG_ENABLED") == "false" { - log.SetOutput(io.Discard) - } - - os.Exit(m.Run()) -} - -func setupTestDB(t *testing.T) *sql.DB { - // Build connection string from environment variables (set by .env.dev) - testUser := os.Getenv("POSTGRES_TEST_USER") - testPassword := os.Getenv("POSTGRES_TEST_PASSWORD") - testPort := os.Getenv("POSTGRES_TEST_PORT") - testDB := os.Getenv("POSTGRES_TEST_DB") - - // Fallback to defaults if not set - if testUser == "" { - testUser = "test_user" - } - if testPassword == "" { - testPassword = "test_password" - } - if testPort == "" { - testPort = "5434" - } - if testDB == "" { - testDB = "coves_test" - } - - dbURL := fmt.Sprintf("postgres://%s:%s@localhost:%s/%s?sslmode=disable", - testUser, testPassword, testPort, testDB) - - db, err := sql.Open("postgres", dbURL) - if err != nil { - t.Fatalf("Failed to connect to test database: %v", err) - } - - // Limit connection pool to prevent "too many clients" error in parallel tests - db.SetMaxOpenConns(5) - db.SetMaxIdleConns(2) - - if pingErr := db.Ping(); pingErr != nil { - t.Fatalf("Failed to ping test database: %v", pingErr) - } - - if dialectErr := goose.SetDialect("postgres"); dialectErr != nil { - t.Fatalf("Failed to set goose dialect: %v", dialectErr) - } - - if migrateErr := goose.Up(db, "../../internal/db/migrations"); migrateErr != nil { - t.Fatalf("Failed to run migrations: %v", migrateErr) - } - - // Clean up any existing test data (order matters due to FK constraints) - // Delete subscriptions first (references communities and users) - _, err = db.Exec("DELETE FROM community_subscriptions") - if err != nil { - t.Logf("Warning: Failed to clean up subscriptions: %v", err) - } - // Delete comments (references posts) - _, err = db.Exec("DELETE FROM comments") - if err != nil { - t.Logf("Warning: Failed to clean up comments: %v", err) - } - // Delete posts (references communities) - _, err = db.Exec("DELETE FROM posts") - if err != nil { - t.Logf("Warning: Failed to clean up posts: %v", err) - } - // Delete communities - _, err = db.Exec("DELETE FROM communities") - if err != nil { - t.Logf("Warning: Failed to clean up communities: %v", err) - } - // Delete users - _, err = db.Exec("DELETE FROM users WHERE handle LIKE '%.test'") - if err != nil { - t.Logf("Warning: Failed to clean up test users: %v", err) - } - - return db + os.Exit(testkit.Main(m, testkit.RequirePostgres, testkit.RequirePDS, testkit.RequireJetstream)) } // generateTestDID generates a unique test DID for integration tests @@ -133,12 +51,7 @@ func generateTestDID(suffix string) string { } func TestUserCreationAndRetrieval(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Wire up dependencies userRepo := postgres.NewUserRepository(db) @@ -203,12 +116,7 @@ func TestUserCreationAndRetrieval(t *testing.T) { } func TestGetProfileEndpoint(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Wire up dependencies userRepo := postgres.NewUserRepository(db) @@ -307,12 +215,7 @@ func TestGetProfileEndpoint(t *testing.T) { // TestDuplicateCreation tests that duplicate DID/handle creation fails properly func TestDuplicateCreation(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) resolver := identity.NewResolver(db, identity.DefaultConfig()) @@ -367,12 +270,7 @@ func TestDuplicateCreation(t *testing.T) { // TestUserRepository_GetByDIDs tests the batch user retrieval functionality func TestUserRepository_GetByDIDs(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) ctx := context.Background() @@ -531,12 +429,7 @@ func TestUserRepository_GetByDIDs(t *testing.T) { // TestProfileStats tests that profile stats are returned correctly func TestProfileStats(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Use unique test DID to avoid conflicts with other test runs uniqueSuffix := time.Now().UnixNano() @@ -679,12 +572,7 @@ func TestProfileStats(t *testing.T) { // TestProfileStats_CommentCount tests that comment counting works correctly func TestProfileStats_CommentCount(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) uniqueSuffix := time.Now().UnixNano() testDID := fmt.Sprintf("did:plc:commentcount%d", uniqueSuffix) @@ -774,12 +662,7 @@ func TestProfileStats_CommentCount(t *testing.T) { // TestProfileStats_CommunityCount tests that subscription counting works correctly func TestProfileStats_CommunityCount(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) uniqueSuffix := time.Now().UnixNano() testDID := fmt.Sprintf("did:plc:subcount%d", uniqueSuffix) @@ -838,12 +721,7 @@ func TestProfileStats_CommunityCount(t *testing.T) { // TestGetProfile_NonExistentDID tests that GetProfile returns appropriate error for non-existent DID func TestGetProfile_NonExistentDID(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) resolver := identity.NewResolver(db, identity.DefaultConfig()) @@ -889,12 +767,7 @@ func TestGetProfile_NonExistentDID(t *testing.T) { // TestProfileStatsEndpoint tests the HTTP endpoint returns stats correctly func TestProfileStatsEndpoint(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Wire up dependencies userRepo := postgres.NewUserRepository(db) @@ -986,12 +859,7 @@ func TestProfileStatsEndpoint(t *testing.T) { // TestHandleValidation tests atProto handle validation rules func TestHandleValidation(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) userRepo := postgres.NewUserRepository(db) resolver := identity.NewResolver(db, identity.DefaultConfig()) @@ -1111,12 +979,7 @@ func TestHandleValidation(t *testing.T) { // TestAccountDeletion_Integration tests the complete account deletion flow // from handler → service → repository with a real database func TestAccountDeletion_Integration(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) uniqueSuffix := time.Now().UnixNano() testDID := fmt.Sprintf("did:plc:deletetest%d", uniqueSuffix) diff --git a/tests/integration/userblock_e2e_test.go b/tests/integration/userblock_e2e_test.go index 9caea9d..6e23420 100644 --- a/tests/integration/userblock_e2e_test.go +++ b/tests/integration/userblock_e2e_test.go @@ -8,6 +8,7 @@ import ( "Coves/internal/atproto/utils" "Coves/internal/core/userblocks" "Coves/internal/db/postgres" + "Coves/tests/testkit" "bytes" "context" "encoding/json" @@ -19,15 +20,13 @@ import ( "time" "github.com/go-chi/chi/v5" - _ "github.com/lib/pq" ) // TestUserBlockE2E_BlockAndUnblock tests the full user block lifecycle with a real PDS. // Flow: Client -> XRPC -> PDS Write -> Verify on PDS -> Jetstream -> Consumer -> AppView // Then: Client -> XRPC Unblock -> PDS Delete -> Jetstream -> Consumer -> AppView removal func TestUserBlockE2E_BlockAndUnblock(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() pdsURL := getTestPDSURL() @@ -43,11 +42,6 @@ func TestUserBlockE2E_BlockAndUnblock(t *testing.T) { } }() - // Clean up user_blocks at the end to avoid polluting other tests - defer func() { - _, _ = db.Exec("DELETE FROM user_blocks") - }() - // Setup repository blockRepo := postgres.NewUserBlockRepository(db) @@ -369,8 +363,7 @@ func TestUserBlockE2E_BlockAndUnblock(t *testing.T) { // TestUserBlockE2E_SelfBlockPrevented tests that a user cannot block themselves. // This validates the self-block guard in the service layer with a real PDS. func TestUserBlockE2E_SelfBlockPrevented(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) pdsURL := getTestPDSURL() @@ -385,11 +378,6 @@ func TestUserBlockE2E_SelfBlockPrevented(t *testing.T) { } }() - // Clean up user_blocks at the end - defer func() { - _, _ = db.Exec("DELETE FROM user_blocks") - }() - // Setup repository blockRepo := postgres.NewUserBlockRepository(db) diff --git a/tests/integration/userblock_enforcement_test.go b/tests/integration/userblock_enforcement_test.go index 7888e5a..17dc273 100644 --- a/tests/integration/userblock_enforcement_test.go +++ b/tests/integration/userblock_enforcement_test.go @@ -8,8 +8,8 @@ import ( "Coves/internal/core/discover" "Coves/internal/core/timeline" "Coves/internal/core/userblocks" + "Coves/tests/testkit" "context" - "database/sql" "fmt" "testing" "time" @@ -22,7 +22,7 @@ import ( // but still shows them to unauthenticated viewers. func TestUserBlock_CommunityFeedFiltering(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) + db := testkit.DB(t) testID := time.Now().UnixNano() @@ -34,16 +34,6 @@ func TestUserBlock_CommunityFeedFiltering(t *testing.T) { communityName := fmt.Sprintf("blockfeed-%d", testID) ownerHandle := fmt.Sprintf("blockfeed-owner-%d.test", testID) - // Cleanup test data after the test - defer func() { - cleanupUserBlockEnforcementTestData(t, db, blockerDID, posterDID, communityName, ownerHandle) - // Also clean up third-party user - _, _ = db.Exec("DELETE FROM users WHERE did = $1", thirdPartyDID) - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() - // Setup: Create test users for _, u := range []struct{ did, handle string }{ {blockerDID, fmt.Sprintf("blocker-%d.bsky.social", testID)}, @@ -184,7 +174,7 @@ func TestUserBlock_CommunityFeedFiltering(t *testing.T) { // but still shows them to unauthenticated viewers. func TestUserBlock_DiscoverFeedFiltering(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) + db := testkit.DB(t) testID := time.Now().UnixNano() @@ -194,14 +184,6 @@ func TestUserBlock_DiscoverFeedFiltering(t *testing.T) { communityName := fmt.Sprintf("blockdisc-%d", testID) ownerHandle := fmt.Sprintf("blockdisc-owner-%d.test", testID) - defer func() { - cleanupUserBlockEnforcementTestData(t, db, blockerDID, posterDID, communityName, ownerHandle) - _, _ = db.Exec("DELETE FROM users WHERE did = $1", thirdPartyDID) - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() - // Setup: Create test users for _, u := range []struct{ did, handle string }{ {blockerDID, fmt.Sprintf("blocker-%d.bsky.social", testID)}, @@ -316,7 +298,7 @@ func TestUserBlock_DiscoverFeedFiltering(t *testing.T) { // returns block records, confirming that GetBlock returns a RecordURI when a block exists. func TestUserBlock_ProfileViewerState(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) + db := testkit.DB(t) testID := time.Now().UnixNano() @@ -324,13 +306,6 @@ func TestUserBlock_ProfileViewerState(t *testing.T) { userB := fmt.Sprintf("did:plc:profile-viewer-b-%d", testID) expectedRecordURI := fmt.Sprintf("at://%s/social.coves.actor.block/profileblock1", userA) - defer func() { - _, _ = db.Exec("DELETE FROM user_blocks WHERE blocker_did = $1", userA) - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() - // Create the user block repo userBlockRepo := postgresRepo.NewUserBlockRepository(db) @@ -385,7 +360,7 @@ func TestUserBlock_ProfileViewerState(t *testing.T) { // filtered out when querying with a viewerDID. func TestUserBlock_CommentFiltering(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) + db := testkit.DB(t) testID := time.Now().UnixNano() @@ -393,15 +368,6 @@ func TestUserBlock_CommentFiltering(t *testing.T) { commenterDID := fmt.Sprintf("did:plc:commenter-%d", testID) otherCommenterDID := fmt.Sprintf("did:plc:othercommenter-%d", testID) - defer func() { - _, _ = db.Exec("DELETE FROM comments WHERE commenter_did IN ($1, $2)", commenterDID, otherCommenterDID) - _, _ = db.Exec("DELETE FROM user_blocks WHERE blocker_did = $1", blockerDID) - _, _ = db.Exec("DELETE FROM users WHERE did IN ($1, $2, $3)", blockerDID, commenterDID, otherCommenterDID) - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() - // Create users for _, u := range []struct{ did, handle string }{ {blockerDID, fmt.Sprintf("blocker-%d.bsky.social", testID)}, @@ -501,7 +467,7 @@ func TestUserBlock_CommentFiltering(t *testing.T) { // blocked users' posts from the authenticated user's timeline feed. func TestUserBlock_TimelineFeedFiltering(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) + db := testkit.DB(t) testID := time.Now().UnixNano() @@ -511,19 +477,6 @@ func TestUserBlock_TimelineFeedFiltering(t *testing.T) { communityName := fmt.Sprintf("blocktl-%d", testID) ownerHandle := fmt.Sprintf("blocktl-owner-%d.test", testID) - defer func() { - communityDID := fmt.Sprintf("did:plc:community-%s", communityName) - _, _ = db.Exec("DELETE FROM user_blocks WHERE blocker_did = $1", viewerDID) - _, _ = db.Exec("DELETE FROM community_subscriptions WHERE user_did = $1", viewerDID) - _, _ = db.Exec("DELETE FROM posts WHERE community_did = $1", communityDID) - _, _ = db.Exec("DELETE FROM communities WHERE did = $1", communityDID) - ownerDID := fmt.Sprintf("did:plc:%s", ownerHandle) - _, _ = db.Exec("DELETE FROM users WHERE did IN ($1, $2, $3, $4)", viewerDID, blockedAuthorDID, otherAuthorDID, ownerDID) - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() - // Create users for _, u := range []struct{ did, handle string }{ {viewerDID, fmt.Sprintf("viewer-%d.bsky.social", testID)}, @@ -653,26 +606,3 @@ func discoverFeedContainsPost(posts []*discover.FeedViewPost, uri string) bool { } return false } - -// cleanupUserBlockEnforcementTestData removes test data created by block enforcement tests -func cleanupUserBlockEnforcementTestData(t *testing.T, db *sql.DB, blockerDID, posterDID, communityName, ownerHandle string) { - t.Helper() - - communityDID := fmt.Sprintf("did:plc:community-%s", communityName) - // createFeedTestCommunity builds ownerDID as "did:plc:{ownerHandle}" — match that here - ownerDID := fmt.Sprintf("did:plc:%s", ownerHandle) - - // Delete in dependency order: blocks, posts, community subscriptions, communities, users - if _, err := db.Exec("DELETE FROM user_blocks WHERE blocker_did = $1 AND blocked_did = $2", blockerDID, posterDID); err != nil { - t.Logf("Warning: Failed to clean up user blocks: %v", err) - } - if _, err := db.Exec("DELETE FROM posts WHERE community_did = $1", communityDID); err != nil { - t.Logf("Warning: Failed to clean up posts: %v", err) - } - if _, err := db.Exec("DELETE FROM communities WHERE did = $1", communityDID); err != nil { - t.Logf("Warning: Failed to clean up communities: %v", err) - } - if _, err := db.Exec("DELETE FROM users WHERE did IN ($1, $2, $3)", blockerDID, posterDID, ownerDID); err != nil { - t.Logf("Warning: Failed to clean up users: %v", err) - } -} diff --git a/tests/integration/userblock_handler_test.go b/tests/integration/userblock_handler_test.go index e599009..95cbf9d 100644 --- a/tests/integration/userblock_handler_test.go +++ b/tests/integration/userblock_handler_test.go @@ -8,6 +8,7 @@ import ( "Coves/internal/core/blobs" "Coves/internal/core/userblocks" "Coves/internal/db/postgres" + "Coves/tests/testkit" "bytes" "context" "encoding/json" @@ -134,17 +135,7 @@ type userBlockTestEnv struct { func setupUserBlockTestServer(t *testing.T) *userBlockTestEnv { t.Helper() - db := setupTestDB(t) - t.Cleanup(func() { - // Clean up user_blocks before closing db - _, _ = db.Exec("DELETE FROM user_blocks") - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }) - - // Clean up user_blocks from any prior runs - _, _ = db.Exec("DELETE FROM user_blocks") + db := testkit.DB(t) repo := postgres.NewUserBlockRepository(db) tracker := newMockPDSTracker() diff --git a/tests/integration/userblock_indexing_test.go b/tests/integration/userblock_indexing_test.go index 7bc490b..2896239 100644 --- a/tests/integration/userblock_indexing_test.go +++ b/tests/integration/userblock_indexing_test.go @@ -5,6 +5,7 @@ package integration import ( "Coves/internal/atproto/jetstream" "Coves/internal/core/userblocks" + "Coves/tests/testkit" "context" "fmt" "testing" @@ -17,8 +18,7 @@ import ( // social.coves.actor.block is properly indexed in the AppView. func TestUserBlockIndexing_CreateEvent(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) consumer := createUserBlockConsumer(t, repo) @@ -87,8 +87,7 @@ func TestUserBlockIndexing_CreateEvent(t *testing.T) { // properly removes a previously indexed block from the AppView. func TestUserBlockIndexing_DeleteEvent(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) consumer := createUserBlockConsumer(t, repo) @@ -159,8 +158,7 @@ func TestUserBlockIndexing_DeleteEvent(t *testing.T) { // twice results in only 1 block (idempotent via ON CONFLICT DO UPDATE). func TestUserBlockIndexing_Idempotent(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) consumer := createUserBlockConsumer(t, repo) @@ -212,8 +210,7 @@ func TestUserBlockIndexing_Idempotent(t *testing.T) { // non-existent block does not error (graceful/idempotent). func TestUserBlockIndexing_DeleteNonExistent(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) consumer := createUserBlockConsumer(t, repo) @@ -250,5 +247,3 @@ func createUserBlockConsumer(t *testing.T, repo userblocks.Repository) *jetstrea t.Helper() return jetstream.NewUserEventConsumer(nil, nil, jetstream.WithUserBlockRepo(repo)) } - -// cleanupUserBlockTestDB is defined in userblock_repo_test.go diff --git a/tests/integration/userblock_repo_test.go b/tests/integration/userblock_repo_test.go index d388e4b..680842f 100644 --- a/tests/integration/userblock_repo_test.go +++ b/tests/integration/userblock_repo_test.go @@ -4,8 +4,8 @@ package integration import ( "Coves/internal/core/userblocks" + "Coves/tests/testkit" "context" - "database/sql" "fmt" "testing" "time" @@ -16,8 +16,7 @@ import ( // TestUserBlockRepo_BlockUser tests creating user blocks func TestUserBlockRepo_BlockUser(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) @@ -120,8 +119,7 @@ func TestUserBlockRepo_BlockUser(t *testing.T) { // TestUserBlockRepo_UnblockUser tests removing user blocks func TestUserBlockRepo_UnblockUser(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) @@ -166,8 +164,7 @@ func TestUserBlockRepo_UnblockUser(t *testing.T) { // TestUserBlockRepo_GetBlock tests block retrieval by blocker + blocked DID func TestUserBlockRepo_GetBlock(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) @@ -215,8 +212,7 @@ func TestUserBlockRepo_GetBlock(t *testing.T) { // TestUserBlockRepo_GetBlockByURI tests block retrieval by record URI func TestUserBlockRepo_GetBlockByURI(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) @@ -261,8 +257,7 @@ func TestUserBlockRepo_GetBlockByURI(t *testing.T) { // TestUserBlockRepo_ListBlockedUsers tests listing blocked users with pagination func TestUserBlockRepo_ListBlockedUsers(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) @@ -357,8 +352,7 @@ func TestUserBlockRepo_ListBlockedUsers(t *testing.T) { // TestUserBlockRepo_IsBlocked tests the fast block check func TestUserBlockRepo_IsBlocked(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) @@ -433,8 +427,7 @@ func TestUserBlockRepo_IsBlocked(t *testing.T) { // TestUserBlockRepo_AreBlocked tests the batch block check func TestUserBlockRepo_AreBlocked(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) @@ -499,8 +492,7 @@ func TestUserBlockRepo_AreBlocked(t *testing.T) { // when processing DELETE operations (which only carry the record URI, not DID pairs). func TestUserBlockRepo_UnblockByRecordURI(t *testing.T) { ctx := context.Background() - db := setupTestDB(t) - defer cleanupUserBlockTestDB(t, db) + db := testkit.DB(t) repo := postgresRepo.NewUserBlockRepository(db) @@ -542,17 +534,3 @@ func TestUserBlockRepo_UnblockByRecordURI(t *testing.T) { t.Errorf("Expected ErrBlockNotFound after unblock-by-URI flow, got: %v", err) } } - -// cleanupUserBlockTestDB removes test data from the user_blocks table -func cleanupUserBlockTestDB(t *testing.T, db *sql.DB) { - t.Helper() - - _, err := db.Exec("DELETE FROM user_blocks WHERE blocker_did LIKE 'did:plc:test-%'") - if err != nil { - t.Logf("Warning: Failed to clean up user blocks: %v", err) - } - - if closeErr := db.Close(); closeErr != nil { - t.Logf("Failed to close database: %v", closeErr) - } -} diff --git a/tests/integration/vote_e2e_test.go b/tests/integration/vote_e2e_test.go index 843257e..f6192d5 100644 --- a/tests/integration/vote_e2e_test.go +++ b/tests/integration/vote_e2e_test.go @@ -8,9 +8,9 @@ import ( "Coves/internal/atproto/utils" "Coves/internal/core/votes" "Coves/internal/db/postgres" + "Coves/tests/testkit" "bytes" "context" - "database/sql" "encoding/json" "fmt" "io" @@ -24,37 +24,13 @@ import ( "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" ) // TestVoteE2E_CreateUpvote tests the full vote creation flow with a real local PDS // Flow: Client → XRPC → PDS Write → Jetstream → Consumer → AppView func TestVoteE2E_CreateUpvote(t *testing.T) { - // Setup test database - dbURL := os.Getenv("TEST_DATABASE_URL") - if dbURL == "" { - dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" - } - - db, err := sql.Open("postgres", dbURL) - if err != nil { - t.Fatalf("Failed to connect to test database: %v", err) - } - defer func() { - if closeErr := db.Close(); closeErr != nil { - t.Logf("Failed to close database: %v", closeErr) - } - }() - - // Run migrations - if dialectErr := goose.SetDialect("postgres"); dialectErr != nil { - t.Fatalf("Failed to set goose dialect: %v", dialectErr) - } - if migrateErr := goose.Up(db, "../../internal/db/migrations"); migrateErr != nil { - t.Fatalf("Failed to run migrations: %v", migrateErr) - } + db := testkit.DB(t) // Check if PDS is running pdsURL := os.Getenv("PDS_URL") @@ -289,8 +265,7 @@ func TestVoteE2E_CreateUpvote(t *testing.T) { // TestVoteE2E_ToggleSameDirection tests voting twice in same direction (toggle off) func TestVoteE2E_ToggleSameDirection(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() pdsURL := getTestPDSURL() @@ -455,8 +430,7 @@ func TestVoteE2E_ToggleSameDirection(t *testing.T) { // TestVoteE2E_ToggleDifferentDirection tests changing vote direction func TestVoteE2E_ToggleDifferentDirection(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() pdsURL := getTestPDSURL() @@ -677,8 +651,7 @@ func TestVoteE2E_ToggleDifferentDirection(t *testing.T) { // TestVoteE2E_DeleteVote tests explicit vote deletion func TestVoteE2E_DeleteVote(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() pdsURL := getTestPDSURL() @@ -864,8 +837,7 @@ func TestVoteE2E_DeleteVote(t *testing.T) { // TestVoteE2E_JetstreamIndexing tests real Jetstream firehose consumption func TestVoteE2E_JetstreamIndexing(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() pdsURL := getTestPDSURL() diff --git a/tests/live/bluesky_post_test.go b/tests/live/bluesky_post_test.go index 76fe4fd..a697a24 100644 --- a/tests/live/bluesky_post_test.go +++ b/tests/live/bluesky_post_test.go @@ -5,6 +5,7 @@ package live import ( "Coves/internal/atproto/identity" "Coves/internal/core/blueskypost" + "Coves/tests/testkit" "context" "database/sql" "testing" @@ -32,8 +33,7 @@ func productionPLCIdentityResolver(db *sql.DB) identity.Resolver { // TestBlueskyPostCrossPosting_URLParsing tests URL detection and parsing func TestBlueskyPostCrossPosting_URLParsing(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) // Use production PLC resolver for real Bluesky handles (READ-ONLY) identityResolver := productionPLCIdentityResolver(db) @@ -107,11 +107,7 @@ func TestBlueskyPostCrossPosting_URLParsing(t *testing.T) { // TestBlueskyPostCrossPosting_LiveAPI tests fetching real posts from Bluesky func TestBlueskyPostCrossPosting_LiveAPI(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Cleanup cache from previous runs - _, _ = db.Exec("DELETE FROM bluesky_post_cache") + db := testkit.DB(t) // Use production PLC resolver for real Bluesky handles (READ-ONLY) identityResolver := productionPLCIdentityResolver(db) @@ -386,8 +382,7 @@ func TestBlueskyPostCrossPosting_CircuitBreaker(t *testing.T) { // We don't actually want to trip the circuit breaker against production, // so this is more of a unit-level integration test. - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) // Use production PLC resolver for real Bluesky handles (READ-ONLY) identityResolver := productionPLCIdentityResolver(db) @@ -423,14 +418,10 @@ func TestBlueskyPostCrossPosting_CircuitBreaker(t *testing.T) { // TestBlueskyPostCrossPosting_E2E_PostCreation tests the full flow of creating a post with a Bluesky embed func TestBlueskyPostCrossPosting_E2E_PostCreation(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() + db := testkit.DB(t) ctx := context.Background() - // Cleanup cache - _, _ = db.Exec("DELETE FROM bluesky_post_cache WHERE at_uri LIKE 'at://did:plc:%'") - // Use production PLC resolver for real Bluesky handles (READ-ONLY) identityResolver := productionPLCIdentityResolver(db) @@ -490,11 +481,7 @@ func TestBlueskyPostCrossPosting_E2E_PostCreation(t *testing.T) { // TestBlueskyPostCrossPosting_EmbedConversion tests that Bluesky URLs in external embeds // are converted to social.coves.embed.post with proper strongRef (uri + cid) func TestBlueskyPostCrossPosting_EmbedConversion(t *testing.T) { - db := setupTestDB(t) - defer func() { _ = db.Close() }() - - // Cleanup cache from previous runs - _, _ = db.Exec("DELETE FROM bluesky_post_cache") + db := testkit.DB(t) // Use production PLC resolver for real Bluesky handles (READ-ONLY) identityResolver := productionPLCIdentityResolver(db) diff --git a/tests/live/helpers.go b/tests/live/helpers.go index 8b357e8..6ea4cef 100644 --- a/tests/live/helpers.go +++ b/tests/live/helpers.go @@ -22,77 +22,9 @@ package live import ( - "database/sql" "fmt" - "os" - "testing" - - _ "github.com/lib/pq" - "github.com/pressly/goose/v3" ) -// setupTestDB connects to the test database, runs migrations, and clears the -// tables these tests write to. Copied from tests/integration so the live tier -// stands alone; the two converge again when the suite moves to testkit.DB(t). -func setupTestDB(t *testing.T) *sql.DB { - testUser := os.Getenv("POSTGRES_TEST_USER") - testPassword := os.Getenv("POSTGRES_TEST_PASSWORD") - testPort := os.Getenv("POSTGRES_TEST_PORT") - testDB := os.Getenv("POSTGRES_TEST_DB") - - if testUser == "" { - testUser = "test_user" - } - if testPassword == "" { - testPassword = "test_password" - } - if testPort == "" { - testPort = "5434" - } - if testDB == "" { - testDB = "coves_test" - } - - dbURL := fmt.Sprintf("postgres://%s:%s@localhost:%s/%s?sslmode=disable", - testUser, testPassword, testPort, testDB) - - db, err := sql.Open("postgres", dbURL) - if err != nil { - t.Fatalf("Failed to connect to test database: %v", err) - } - - db.SetMaxOpenConns(5) - db.SetMaxIdleConns(2) - - if pingErr := db.Ping(); pingErr != nil { - t.Fatalf("Failed to ping test database: %v", pingErr) - } - - if dialectErr := goose.SetDialect("postgres"); dialectErr != nil { - t.Fatalf("Failed to set goose dialect: %v", dialectErr) - } - - if migrateErr := goose.Up(db, "../../internal/db/migrations"); migrateErr != nil { - t.Fatalf("Failed to run migrations: %v", migrateErr) - } - - // Order matters: foreign keys point from subscriptions → communities/users, - // comments → posts, posts → communities. - for _, stmt := range []string{ - "DELETE FROM community_subscriptions", - "DELETE FROM comments", - "DELETE FROM posts", - "DELETE FROM communities", - "DELETE FROM users WHERE handle LIKE '%.test'", - } { - if _, err := db.Exec(stmt); err != nil { - t.Logf("Warning: cleanup %q failed: %v", stmt, err) - } - } - - return db -} - // generateTestDID builds a valid did:plc-shaped string for fixtures that never // need PLC registration. func generateTestDID(suffix string) string { diff --git a/tests/live/identity_resolution_test.go b/tests/live/identity_resolution_test.go index 6917d97..d6fb5c2 100644 --- a/tests/live/identity_resolution_test.go +++ b/tests/live/identity_resolution_test.go @@ -4,6 +4,7 @@ package live import ( "Coves/internal/atproto/identity" + "Coves/tests/testkit" "context" "database/sql" "testing" @@ -13,11 +14,12 @@ import ( // purgeIdentityCache removes any cached rows for the given identifiers, so the // next resolution provably goes to the network. // -// The identity cache is a table in the shared test database and setupTestDB -// does not clear it, so without this a run inherits rows from every previous -// run: these tests would resolve entirely from Postgres and still pass with -// the public internet unplugged, which is the one thing the live tier exists -// to rule out. +// testkit.DB hands each test an empty identity_cache, so this is no longer +// about inheriting rows from a previous run. It is about the resolutions this +// test itself performs: the cache is written on every successful resolve, and +// a second lookup of the same identifier would be served from Postgres. These +// tests would then pass with the public internet unplugged, which is the one +// thing the live tier exists to rule out. func purgeIdentityCache(t *testing.T, db *sql.DB, identifiers ...string) { t.Helper() for _, id := range identifiers { @@ -35,12 +37,7 @@ func purgeIdentityCache(t *testing.T, db *sql.DB, identifiers ...string) { // run and therefore never verified anything. Under -tags live it runs // unconditionally and fails when the public network is unreachable. func TestIdentityResolverRealHandles(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) resolver := identity.NewResolver(db, identity.Config{ PLCURL: "https://plc.directory", @@ -128,12 +125,7 @@ func TestIdentityResolverRealHandles(t *testing.T) { // TestResolveDID resolves a real DID document from the production PLC // directory. Opt-in via the `live` build tag; see TestIdentityResolverRealHandles. func TestResolveDID(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) resolver := identity.NewResolver(db, identity.Config{ PLCURL: "https://plc.directory", @@ -143,10 +135,9 @@ func TestResolveDID(t *testing.T) { ctx := context.Background() t.Run("Resolve Real DID Document", func(t *testing.T) { - // TestIdentityResolverRealHandles resolves the same handle and leaves it - // in the identity cache, which setupTestDB does not clear — so without - // this purge the resolution below is served from Postgres and the test - // proves nothing about the production PLC directory. + // Nothing in this test has resolved bsky.app yet, but the purge states + // the requirement the assertion below enforces: this resolution must + // reach the production PLC directory, not the identity cache. purgeIdentityCache(t, db, "bsky.app") // First resolve a handle to get a real DID diff --git a/tests/live/post_unfurl_test.go b/tests/live/post_unfurl_test.go index 52cf7de..df0a6ef 100644 --- a/tests/live/post_unfurl_test.go +++ b/tests/live/post_unfurl_test.go @@ -10,6 +10,7 @@ import ( "Coves/internal/core/unfurl" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "testing" "time" @@ -20,12 +21,7 @@ import ( // TestPostUnfurl_Streamable tests that a post with a Streamable URL gets unfurled func TestPostUnfurl_Streamable(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -66,12 +62,6 @@ func TestPostUnfurl_Streamable(t *testing.T) { "http://localhost:3001", ) - // Cleanup old test data - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:test%'") - _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:test%'") - _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:test%'") - _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%streamable.com%'") - // Create test user testUserDID := generateTestDID("unfurlauthor") testUserHandle := "unfurlauthor.test" @@ -164,12 +154,7 @@ func TestPostUnfurl_Streamable(t *testing.T) { // TestPostUnfurl_YouTube tests that a post with a YouTube URL gets unfurled func TestPostUnfurl_YouTube(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -180,9 +165,6 @@ func TestPostUnfurl_YouTube(t *testing.T) { unfurl.WithCacheTTL(24*time.Hour), ) - // Cleanup cache - _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%youtube.com%'") - // Test YouTube URL youtubeURL := "https://www.youtube.com/watch?v=dQw4w9WgXcQ" @@ -207,12 +189,7 @@ func TestPostUnfurl_YouTube(t *testing.T) { // TestPostUnfurl_Reddit tests that a post with a Reddit URL gets unfurled func TestPostUnfurl_Reddit(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -223,9 +200,6 @@ func TestPostUnfurl_Reddit(t *testing.T) { unfurl.WithCacheTTL(24*time.Hour), ) - // Cleanup cache - _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%reddit.com%'") - // Use a well-known public Reddit post redditURL := "https://www.reddit.com/r/programming/comments/1234/test/" @@ -249,12 +223,7 @@ func TestPostUnfurl_Reddit(t *testing.T) { // TestPostUnfurl_CacheHit tests that the second post with the same URL uses cache func TestPostUnfurl_CacheHit(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -267,7 +236,6 @@ func TestPostUnfurl_CacheHit(t *testing.T) { // Cleanup cache testURL := "https://streamable.com/test123" - _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url = $1", testURL) // First unfurl - should hit network t.Log("First unfurl - expecting cache miss") @@ -311,12 +279,7 @@ func TestPostUnfurl_CacheHit(t *testing.T) { // TestPostUnfurl_UserProvidedMetadata tests that user-provided metadata is preserved func TestPostUnfurl_UserProvidedMetadata(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -355,12 +318,6 @@ func TestPostUnfurl_UserProvidedMetadata(t *testing.T) { "http://localhost:3001", ) - // Cleanup - _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:metadata%'") - _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:metadata%'") - _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:metadata%'") - _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%streamable.com%'") - // Create test user and community testUserDID := generateTestDID("metadatauser") _, err := userService.CreateUser(ctx, users.CreateUserRequest{ @@ -422,12 +379,7 @@ func TestPostUnfurl_UserProvidedMetadata(t *testing.T) { // TestPostUnfurl_OpenGraph tests that OpenGraph URLs get unfurled func TestPostUnfurl_OpenGraph(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -473,12 +425,7 @@ func TestPostUnfurl_OpenGraph(t *testing.T) { // TestPostUnfurl_SmartRouting tests that oEmbed still works while OpenGraph handles others func TestPostUnfurl_SmartRouting(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() @@ -489,9 +436,6 @@ func TestPostUnfurl_SmartRouting(t *testing.T) { unfurl.WithCacheTTL(24*time.Hour), ) - // Clean cache - _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%youtube.com%' OR url LIKE '%wikipedia.org%'") - tests := []struct { name string url string @@ -529,12 +473,7 @@ func TestPostUnfurl_SmartRouting(t *testing.T) { // TestPostUnfurl_KagiKite tests that Kagi Kite URLs get unfurled with story images func TestPostUnfurl_KagiKite(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) // Note: This test requires network access to kite.kagi.com // It will be skipped if the URL is not reachable diff --git a/tests/live/user_resolution_test.go b/tests/live/user_resolution_test.go index cc2ce1e..a308f62 100644 --- a/tests/live/user_resolution_test.go +++ b/tests/live/user_resolution_test.go @@ -6,6 +6,7 @@ import ( "Coves/internal/atproto/identity" "Coves/internal/core/users" "Coves/internal/db/postgres" + "Coves/tests/testkit" "context" "testing" ) @@ -18,12 +19,7 @@ import ( // READ-ONLY: ResolveHandleToDID performs HTTP GET lookups only. It never // registers or mutates anything on the production directory. func TestResolveHandleToDID_RealHandle(t *testing.T) { - db := setupTestDB(t) - defer func() { - if err := db.Close(); err != nil { - t.Logf("Failed to close database: %v", err) - } - }() + db := testkit.DB(t) ctx := context.Background() userRepo := postgres.NewUserRepository(db)