protection_transition_database_test.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. //go:build protectionintegration
  2. package repository
  3. import (
  4. "context"
  5. "maps"
  6. "testing"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/stretchr/testify/require"
  9. )
  10. func TestProtectionTransitionDatabaseSingleCommitAndRollback(t *testing.T) {
  11. client, db := protectionTestDatabase(t)
  12. repo := newAccountRepositoryWithSQL(client, db, nil)
  13. a := &service.Account{Name: "transition", Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth, Concurrency: 32, Status: service.StatusActive}
  14. require.NoError(t, repo.Create(context.Background(), a))
  15. _, err := db.Exec(`CREATE TABLE policy_observations(enabled BOOLEAN, mode TEXT);
  16. CREATE FUNCTION observe_policy() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN INSERT INTO policy_observations VALUES ((NEW.extra->>'anti_degradation')::boolean,NEW.extra#>>'{anti_degrade,mode}'); RETURN NEW; END $$;
  17. CREATE TRIGGER observe_policy AFTER UPDATE OF extra ON accounts FOR EACH ROW EXECUTE FUNCTION observe_policy();`)
  18. require.NoError(t, err)
  19. svc := service.NewAntiDegradeService(protectionIntegrationStore{repo})
  20. changed, err := svc.ApplyAntiDegradeMode(context.Background(), a.ID, service.AntiDegradeMode1)
  21. require.NoError(t, err)
  22. require.True(t, changed.IsMode1ProtectionEnabled())
  23. var count, disabled int
  24. require.NoError(t, db.QueryRow(`SELECT COUNT(*),COUNT(*) FILTER(WHERE NOT enabled) FROM policy_observations`).Scan(&count, &disabled))
  25. require.Equal(t, 1, count)
  26. require.Zero(t, disabled)
  27. _, err = db.Exec(`CREATE FUNCTION reject_mode2() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.extra#>>'{anti_degrade,mode}'='mode2' THEN RAISE EXCEPTION 'test-rejected'; END IF; RETURN NEW; END $$;
  28. CREATE TRIGGER reject_mode2 BEFORE UPDATE OF extra ON accounts FOR EACH ROW EXECUTE FUNCTION reject_mode2();`)
  29. require.NoError(t, err)
  30. _, err = svc.ApplyAntiDegradeMode(context.Background(), a.ID, service.AntiDegradeMode2)
  31. require.ErrorContains(t, err, "test-rejected")
  32. current, err := repo.GetByID(context.Background(), a.ID)
  33. require.NoError(t, err)
  34. require.True(t, current.IsMode1ProtectionEnabled())
  35. require.Equal(t, changed.Extra, current.Extra)
  36. require.NoError(t, db.QueryRow(`SELECT COUNT(*) FROM policy_observations`).Scan(&count))
  37. require.Equal(t, 1, count, "failed transaction must not publish an intermediate state")
  38. }
  39. type staleProtectionStore struct {
  40. protectionIntegrationStore
  41. stale *service.Account
  42. }
  43. func TestProtectedProxyConflictIsCheckedUnderDatabaseLock(t *testing.T) {
  44. client, db := protectionTestDatabase(t)
  45. repo := newAccountRepositoryWithSQL(client, db, nil)
  46. ctx := context.Background()
  47. a := &service.Account{Name: "proxy-conflict", Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth, Concurrency: 4, Status: service.StatusActive}
  48. require.NoError(t, repo.Create(ctx, a))
  49. stale, err := repo.GetByID(ctx, a.ID)
  50. require.NoError(t, err)
  51. stale.Extra = map[string]any{"proxy_mode": "random"}
  52. stale.ProxyID = nil
  53. require.ErrorIs(t, repo.Update(ctx, stale), service.ErrProtectedProxyModeChange)
  54. actual, err := repo.GetByID(ctx, a.ID)
  55. require.NoError(t, err)
  56. require.Equal(t, "legacy", actual.ProtectionMode())
  57. require.False(t, actual.IsRandomProxy())
  58. clear := int64(0)
  59. _, err = repo.BulkUpdate(ctx, []int64{a.ID}, service.AccountBulkUpdate{ProxyID: &clear, Extra: map[string]any{"proxy_mode": "random"}})
  60. require.ErrorIs(t, err, service.ErrProtectedProxyModeChange)
  61. actual, err = repo.GetByID(ctx, a.ID)
  62. require.NoError(t, err)
  63. require.Equal(t, "legacy", actual.ProtectionMode())
  64. }
  65. func (s staleProtectionStore) GetAccount(context.Context, int64) (*service.Account, error) {
  66. return s.stale, nil
  67. }
  68. func TestProtectionTransitionDatabaseRejectsConcurrentRevision(t *testing.T) {
  69. client, db := protectionTestDatabase(t)
  70. repo := newAccountRepositoryWithSQL(client, db, nil)
  71. a := &service.Account{Name: "revision", Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth, Concurrency: 16, Status: service.StatusActive}
  72. require.NoError(t, repo.Create(context.Background(), a))
  73. stale, err := repo.GetByID(context.Background(), a.ID)
  74. require.NoError(t, err)
  75. _, err = db.Exec(`UPDATE accounts SET notes='concurrent edit',updated_at=updated_at+INTERVAL '1 second' WHERE id=$1`, a.ID)
  76. require.NoError(t, err)
  77. svc := service.NewAntiDegradeService(staleProtectionStore{protectionIntegrationStore{repo}, stale})
  78. _, err = svc.ApplyAntiDegradeMode(context.Background(), a.ID, service.AntiDegradeMode1)
  79. require.ErrorIs(t, err, service.ErrProtectionConflict)
  80. current, err := repo.GetByID(context.Background(), a.ID)
  81. require.NoError(t, err)
  82. require.Equal(t, "legacy", current.ProtectionMode())
  83. require.Equal(t, "concurrent edit", *current.Notes)
  84. }
  85. func TestAccountProtectionAllStrategiesPreserveBulkAndEmptyStaleWrites(t *testing.T) {
  86. client, db := protectionTestDatabase(t)
  87. repo := newAccountRepositoryWithSQL(client, db, nil)
  88. svc := service.NewAntiDegradeService(protectionIntegrationStore{repo})
  89. ctx := context.Background()
  90. for _, mode := range service.RegisteredProtectionModes() {
  91. t.Run(mode, func(t *testing.T) {
  92. a := &service.Account{Name: mode, Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth, Concurrency: 32, Status: service.StatusActive}
  93. require.NoError(t, repo.Create(ctx, a))
  94. before, err := svc.ApplyAntiDegradeMode(ctx, a.ID, service.AntiDegradeMode(mode))
  95. require.NoError(t, err)
  96. attack := map[string]any{"anti_degradation": false, "anti_degrade": nil, "codex_fingerprint_mode": "off", "enable_tls_fingerprint": false, "tls_fingerprint_builtin": nil}
  97. require.NoError(t, repo.UpdateExtra(ctx, a.ID, attack))
  98. conc := 999
  99. _, err = repo.BulkUpdate(ctx, []int64{a.ID}, service.AccountBulkUpdate{Concurrency: &conc, Extra: attack})
  100. require.NoError(t, err)
  101. stale := *before
  102. stale.Extra = nil
  103. stale.Concurrency = 999
  104. require.NoError(t, repo.Update(ctx, &stale))
  105. after, err := repo.GetByID(ctx, a.ID)
  106. require.NoError(t, err)
  107. expectedExtra := maps.Clone(before.Extra)
  108. expectedMarker := maps.Clone(before.Extra[service.AntiDegradeMarkerExtraKey].(map[string]any))
  109. expectedMarker["max_concurrency"] = float64(conc)
  110. expectedExtra[service.AntiDegradeMarkerExtraKey] = expectedMarker
  111. require.Equal(t, expectedExtra, after.Extra)
  112. require.Equal(t, conc, after.Concurrency)
  113. })
  114. }
  115. }