codex_fingerprint_seed_migration_integration_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. //go:build integration
  2. package repository
  3. import (
  4. "context"
  5. "testing"
  6. "github.com/Wei-Shaw/sub2api/internal/service"
  7. dbmigrations "github.com/Wei-Shaw/sub2api/migrations"
  8. "github.com/google/uuid"
  9. "github.com/lib/pq"
  10. "github.com/stretchr/testify/require"
  11. )
  12. func requireCanonicalUUIDString(t *testing.T, value string) {
  13. t.Helper()
  14. parsed, err := uuid.Parse(value)
  15. require.NoError(t, err)
  16. require.NotEqual(t, uuid.Nil, parsed)
  17. require.Equal(t, parsed.String(), value)
  18. }
  19. func TestMigration225BackfillsOnlyEnabledOpenAIOAuthMissingOrMalformedSeeds(t *testing.T) {
  20. tx := testTx(t)
  21. ctx := context.Background()
  22. migrationSQL, err := dbmigrations.FS.ReadFile("225_backfill_codex_fingerprint_seed.sql")
  23. require.NoError(t, err)
  24. var missingID, blankID, malformedID, validID, offID, apiKeyID int64
  25. require.NoError(t, tx.QueryRowContext(ctx, `
  26. INSERT INTO accounts (name, platform, type, extra)
  27. VALUES ('migration-225-missing', 'openai', 'oauth', '{"codex_fingerprint_mode":"session"}'::jsonb)
  28. RETURNING id
  29. `).Scan(&missingID))
  30. require.NoError(t, tx.QueryRowContext(ctx, `
  31. INSERT INTO accounts (name, platform, type, extra)
  32. VALUES ('migration-225-blank', 'openai', 'oauth', '{"codex_fingerprint_mode":"device","codex_fingerprint_seed":""}'::jsonb)
  33. RETURNING id
  34. `).Scan(&blankID))
  35. require.NoError(t, tx.QueryRowContext(ctx, `
  36. INSERT INTO accounts (name, platform, type, extra)
  37. VALUES ('migration-225-malformed', 'openai', 'oauth', '{"codex_fingerprint_mode":"full","codex_fingerprint_seed":"BAD"}'::jsonb)
  38. RETURNING id
  39. `).Scan(&malformedID))
  40. require.NoError(t, tx.QueryRowContext(ctx, `
  41. INSERT INTO accounts (name, platform, type, extra)
  42. VALUES ('migration-225-valid', 'openai', 'oauth', '{"codex_fingerprint_mode":"session","codex_fingerprint_seed":"11111111-1111-4111-8111-111111111111"}'::jsonb)
  43. RETURNING id
  44. `).Scan(&validID))
  45. require.NoError(t, tx.QueryRowContext(ctx, `
  46. INSERT INTO accounts (name, platform, type, extra)
  47. VALUES ('migration-225-off', 'openai', 'oauth', '{"codex_fingerprint_mode":"off"}'::jsonb)
  48. RETURNING id
  49. `).Scan(&offID))
  50. require.NoError(t, tx.QueryRowContext(ctx, `
  51. INSERT INTO accounts (name, platform, type, extra)
  52. VALUES ('migration-225-apikey', 'openai', 'apikey', '{"codex_fingerprint_mode":"session"}'::jsonb)
  53. RETURNING id
  54. `).Scan(&apiKeyID))
  55. _, err = tx.ExecContext(ctx, string(migrationSQL))
  56. require.NoError(t, err)
  57. seedsAfterFirst := map[int64]string{}
  58. for _, id := range []int64{missingID, blankID, malformedID, validID} {
  59. var seed string
  60. require.NoError(t, tx.QueryRowContext(ctx, `SELECT extra->>'codex_fingerprint_seed' FROM accounts WHERE id = $1`, id).Scan(&seed))
  61. requireCanonicalUUIDString(t, seed)
  62. seedsAfterFirst[id] = seed
  63. }
  64. require.Equal(t, "11111111-1111-4111-8111-111111111111", seedsAfterFirst[validID])
  65. for _, id := range []int64{offID, apiKeyID} {
  66. var hasSeed bool
  67. require.NoError(t, tx.QueryRowContext(ctx, `SELECT extra ? 'codex_fingerprint_seed' FROM accounts WHERE id = $1`, id).Scan(&hasSeed))
  68. require.False(t, hasSeed)
  69. }
  70. _, err = tx.ExecContext(ctx, string(migrationSQL))
  71. require.NoError(t, err)
  72. for id, want := range seedsAfterFirst {
  73. var got string
  74. require.NoError(t, tx.QueryRowContext(ctx, `SELECT extra->>'codex_fingerprint_seed' FROM accounts WHERE id = $1`, id).Scan(&got))
  75. require.Equal(t, want, got)
  76. }
  77. }
  78. func TestBulkUpdateGeneratesDistinctStableCodexFingerprintSeedsPerEligibleRow(t *testing.T) {
  79. ctx := context.Background()
  80. testName := "bulk-codex-seed-" + uuid.NewString()
  81. type fixture struct {
  82. name string
  83. accountType string
  84. extra string
  85. }
  86. fixtures := []fixture{
  87. {name: testName + "-missing-a", accountType: service.AccountTypeOAuth, extra: `{}`},
  88. {name: testName + "-missing-b", accountType: service.AccountTypeOAuth, extra: `{"codex_fingerprint_seed":"BAD"}`},
  89. {name: testName + "-valid", accountType: service.AccountTypeOAuth, extra: `{"codex_fingerprint_seed":"11111111-1111-4111-8111-111111111111"}`},
  90. {name: testName + "-apikey", accountType: service.AccountTypeAPIKey, extra: `{}`},
  91. }
  92. ids := make([]int64, 0, len(fixtures))
  93. for _, f := range fixtures {
  94. var id int64
  95. require.NoError(t, integrationDB.QueryRowContext(ctx, `
  96. INSERT INTO accounts (name, platform, type, extra)
  97. VALUES ($1, 'openai', $2, $3::jsonb)
  98. RETURNING id
  99. `, f.name, f.accountType, f.extra).Scan(&id))
  100. ids = append(ids, id)
  101. }
  102. t.Cleanup(func() {
  103. _, _ = integrationDB.ExecContext(context.Background(), `DELETE FROM scheduler_outbox WHERE account_id = ANY($1)`, pq.Array(ids))
  104. _, _ = integrationDB.ExecContext(context.Background(), `DELETE FROM accounts WHERE id = ANY($1)`, pq.Array(ids))
  105. })
  106. repo := newAccountRepositoryWithSQL(testEntClient(t), integrationDB, nil)
  107. updates := service.AccountBulkUpdate{
  108. Extra: map[string]any{
  109. "codex_fingerprint_mode": "session",
  110. },
  111. EnsureCodexFingerprintSeed: true,
  112. }
  113. rows, err := repo.BulkUpdate(ctx, ids, updates)
  114. require.NoError(t, err)
  115. require.Equal(t, int64(len(ids)), rows)
  116. readSeed := func(id int64) string {
  117. t.Helper()
  118. var seed string
  119. require.NoError(t, integrationDB.QueryRowContext(ctx, `SELECT COALESCE(extra->>'codex_fingerprint_seed', '') FROM accounts WHERE id = $1`, id).Scan(&seed))
  120. return seed
  121. }
  122. firstSeeds := []string{readSeed(ids[0]), readSeed(ids[1]), readSeed(ids[2]), readSeed(ids[3])}
  123. requireCanonicalUUIDString(t, firstSeeds[0])
  124. requireCanonicalUUIDString(t, firstSeeds[1])
  125. require.NotEqual(t, firstSeeds[0], firstSeeds[1], "gen_random_uuid must be evaluated per eligible row")
  126. require.Equal(t, "11111111-1111-4111-8111-111111111111", firstSeeds[2])
  127. require.Empty(t, firstSeeds[3], "API-key accounts must not receive a Codex fingerprint seed")
  128. rows, err = repo.BulkUpdate(ctx, ids, updates)
  129. require.NoError(t, err)
  130. require.Equal(t, int64(len(ids)), rows)
  131. for i, want := range firstSeeds {
  132. require.Equal(t, want, readSeed(ids[i]), "retry must not rotate an existing valid seed")
  133. }
  134. }