package store_test import ( "context" "testing" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/require" ) // seedIdentity inserts the un-RLS'd dex_subject → user_id mapping for a user, so // the cascade-on-delete to user_identities can be asserted. func seedIdentity(t *testing.T, p *pgxpool.Pool, subject, userID string) { t.Helper() _, err := p.Exec(context.Background(), `INSERT INTO user_identities (dex_subject, user_id) VALUES ($1, $2)`, subject, userID) require.NoError(t, err) } // countFor counts rows owned by userID in table. The users table is keyed on its // own id; every other isolated table on user_id. func countFor(t *testing.T, p *pgxpool.Pool, table, userID string) int { t.Helper() col := "user_id" if table == "users" { col = "id" } var n int require.NoError(t, p.QueryRow(context.Background(), `SELECT count(*) FROM `+table+` WHERE `+col+` = $1`, userID).Scan(&n)) return n } func countDeliveries(t *testing.T, p *pgxpool.Pool, summaryID string) int { t.Helper() var n int require.NoError(t, p.QueryRow(context.Background(), `SELECT count(*) FROM sink_deliveries WHERE summary_id = $1`, summaryID).Scan(&n)) return n } func countIdentities(t *testing.T, p *pgxpool.Pool, userID string) int { t.Helper() var n int require.NoError(t, p.QueryRow(context.Background(), `SELECT count(*) FROM user_identities WHERE user_id = $1`, userID).Scan(&n)) return n } // TestDeleteUserRemovesAllRowsForUserOnly is the account-deletion isolation proof // (Worker N+M): DeleteUser wipes every row owned by the target user — across the // cascade-linked tables, the user_identities map (ON DELETE CASCADE), AND // summary_actions (which has NO FK to users, so the users-row cascade does not // reach it and DeleteUser must delete it explicitly) — while leaving another // user's rows completely intact. func TestDeleteUserRemovesAllRowsForUserOnly(t *testing.T) { ctx := context.Background() newStore(t) // apply migrations super := rawPool(t) resetDB(t, super) a := seedUser(t, super, userA) b := seedUser(t, super, userB) seedIdentity(t, super, "subject-a", userA) seedIdentity(t, super, "subject-b", userB) s := newStore(t) require.NoError(t, s.DeleteUser(ctx, userA)) // Every user-keyed isolated table: zero rows for A, exactly one for B. for _, table := range userIsolatedTables { require.Equal(t, 0, countFor(t, super, table, userA), "A's %s rows must be deleted", table) require.Equal(t, 1, countFor(t, super, table, userB), "B's %s rows must survive A's deletion", table) } // sink_deliveries is keyed by summary, not user_id (cascade from summaries). require.Equal(t, 0, countDeliveries(t, super, a.summaryID), "A's deliveries must cascade-delete") require.Equal(t, 1, countDeliveries(t, super, b.summaryID), "B's deliveries must survive") // The cascade must reach user_identities (explicitly asserted per the mission). require.Equal(t, 0, countIdentities(t, super, userA), "A's identity mapping must cascade-delete") require.Equal(t, 1, countIdentities(t, super, userB), "B's identity mapping must survive") } func TestDeleteUserIsIdempotent(t *testing.T) { ctx := context.Background() s := newStore(t) resetDB(t, rawPool(t)) // Deleting an absent user is a no-op, not an error. require.NoError(t, s.DeleteUser(ctx, userA)) } func TestDisplayNameReturnsRegisteredName(t *testing.T) { ctx := context.Background() s := newStore(t) p := rawPool(t) resetDB(t, p) _, err := p.Exec(ctx, `INSERT INTO users (id, display_name) VALUES ($1, $2)`, userA, "Ada") require.NoError(t, err) name, err := s.DisplayName(ctx, userA) require.NoError(t, err) require.Equal(t, "Ada", name) }