account_protection_integration_test.go 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. //go:build protectionintegration
  2. package repository
  3. import (
  4. "context"
  5. "fmt"
  6. "testing"
  7. "time"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. "github.com/stretchr/testify/require"
  10. )
  11. type protectionIntegrationStore struct{ repo *accountRepository }
  12. func (s protectionIntegrationStore) GetAccount(ctx context.Context, id int64) (*service.Account, error) {
  13. return s.repo.GetByID(ctx, id)
  14. }
  15. func (s protectionIntegrationStore) UpdateAccount(ctx context.Context, id int64, input *service.UpdateAccountInput) (*service.Account, error) {
  16. a, err := s.repo.GetByID(ctx, id)
  17. if err != nil {
  18. return nil, err
  19. }
  20. if input.Extra != nil {
  21. a.Extra = input.Extra
  22. }
  23. if input.Concurrency != nil {
  24. a.Concurrency = *input.Concurrency
  25. }
  26. err = s.repo.Update(ctx, a)
  27. if err != nil {
  28. return nil, err
  29. }
  30. return s.repo.GetByID(ctx, id)
  31. }
  32. func TestAccountProtectionCreateImportAndStaleWrites(t *testing.T) {
  33. ctx := context.Background()
  34. client, protectionDB := protectionTestDatabase(t)
  35. repo := newAccountRepositoryWithSQL(client, protectionDB, nil)
  36. for _, kind := range []string{service.AccountTypeOAuth, service.AccountTypeAPIKey} {
  37. t.Run(kind, func(t *testing.T) {
  38. a := &service.Account{Name: fmt.Sprintf("protection-%s-%d", kind, time.Now().UnixNano()), Platform: service.PlatformOpenAI, Type: kind, Concurrency: 32, Status: service.StatusActive, Extra: map[string]any{"anti_degradation": false}}
  39. require.NoError(t, repo.Create(ctx, a))
  40. t.Cleanup(func() {
  41. _, _ = protectionDB.ExecContext(context.Background(), "DELETE FROM scheduler_outbox WHERE account_id=$1", a.ID)
  42. _, _ = protectionDB.ExecContext(context.Background(), "DELETE FROM accounts WHERE id=$1", a.ID)
  43. })
  44. created, err := repo.GetByID(ctx, a.ID)
  45. require.NoError(t, err)
  46. require.True(t, created.AntiDegradationEnabled())
  47. require.Equal(t, 32, created.Concurrency)
  48. require.NoError(t, repo.UpdateExtra(ctx, a.ID, map[string]any{"anti_degradation": false, "anti_degrade": nil, "untouched": "partial"}))
  49. bulkConcurrency := 100
  50. _, err = repo.BulkUpdate(ctx, []int64{a.ID}, service.AccountBulkUpdate{Concurrency: &bulkConcurrency, Extra: map[string]any{"anti_degradation": false, "anti_degrade": nil}})
  51. require.NoError(t, err)
  52. updated, err := repo.GetByID(ctx, a.ID)
  53. require.NoError(t, err)
  54. require.True(t, updated.AntiDegradationEnabled())
  55. require.Equal(t, bulkConcurrency, updated.Concurrency)
  56. require.Equal(t, "partial", updated.Extra["untouched"])
  57. svc := service.NewAntiDegradeService(protectionIntegrationStore{repo})
  58. _, err = svc.SetProtection(ctx, a.ID, false, false)
  59. require.Error(t, err)
  60. _, err = svc.SetProtection(ctx, a.ID, false, true)
  61. require.NoError(t, err)
  62. // A request loaded before disable must not re-enable it after a delay.
  63. created.Name += " stale-refresh"
  64. require.NoError(t, repo.Update(ctx, created))
  65. disabled, err := repo.GetByID(ctx, a.ID)
  66. require.NoError(t, err)
  67. require.False(t, disabled.AntiDegradationEnabled())
  68. _, err = svc.SetProtection(ctx, a.ID, true, false)
  69. require.NoError(t, err)
  70. // And a stale OFF import cannot undo the later explicit enable.
  71. disabled.Extra = map[string]any{"anti_degradation": false, "import": "kept"}
  72. require.NoError(t, repo.Update(ctx, disabled))
  73. final, err := repo.GetByID(ctx, a.ID)
  74. require.NoError(t, err)
  75. require.True(t, final.AntiDegradationEnabled())
  76. require.Equal(t, "kept", final.Extra["import"])
  77. })
  78. }
  79. }