//go:build unit package service import ( "context" "net/http" "testing" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/stretchr/testify/require" ) // dbFallbackRepoStub extends errorPolicyRepoStub with a configurable DB account // returned by GetByID, simulating cache miss + DB fallback. type dbFallbackRepoStub struct { errorPolicyRepoStub dbAccount *Account // returned by GetByID when non-nil } func (r *dbFallbackRepoStub) GetByID(ctx context.Context, id int64) (*Account, error) { if r.dbAccount != nil && r.dbAccount.ID == id { return r.dbAccount, nil } return nil, nil // not found, no error } func TestCheckErrorPolicy_401_DBFallback_Escalates(t *testing.T) { // Scenario: cache account has empty TempUnschedulableReason (cache miss), // but DB account has a previous 401 record → should escalate to ErrorPolicyNone. repo := &dbFallbackRepoStub{ dbAccount: &Account{ ID: 20, TempUnschedulableReason: `{"status_code":401,"until_unix":1735689600}`, }, } svc := NewRateLimitService(repo, nil, &config.Config{}, nil, nil) account := &Account{ ID: 20, Type: AccountTypeOAuth, Platform: PlatformAntigravity, TempUnschedulableReason: "", // cache miss — reason is empty Credentials: map[string]any{ "temp_unschedulable_enabled": true, "temp_unschedulable_rules": []any{ map[string]any{ "error_code": float64(401), "keywords": []any{"unauthorized"}, "duration_minutes": float64(10), }, }, }, } result := svc.CheckErrorPolicy(context.Background(), account, http.StatusUnauthorized, []byte(`unauthorized`)) require.Equal(t, ErrorPolicyNone, result, "401 with DB fallback showing previous 401 should escalate to ErrorPolicyNone") } func TestCheckErrorPolicy_401_DBFallback_NoDBRecord_FirstHit(t *testing.T) { // Scenario: cache account has empty TempUnschedulableReason, // DB also has no previous 401 record → should NOT escalate (first hit → temp unscheduled). repo := &dbFallbackRepoStub{ dbAccount: &Account{ ID: 21, TempUnschedulableReason: "", // DB also empty }, } svc := NewRateLimitService(repo, nil, &config.Config{}, nil, nil) account := &Account{ ID: 21, Type: AccountTypeOAuth, Platform: PlatformAntigravity, TempUnschedulableReason: "", Credentials: map[string]any{ "temp_unschedulable_enabled": true, "temp_unschedulable_rules": []any{ map[string]any{ "error_code": float64(401), "keywords": []any{"unauthorized"}, "duration_minutes": float64(10), }, }, }, } result := svc.CheckErrorPolicy(context.Background(), account, http.StatusUnauthorized, []byte(`unauthorized`)) require.Equal(t, ErrorPolicyTempUnscheduled, result, "401 first hit with no DB record should temp-unschedule") } func TestCheckErrorPolicy_401_DBFallback_DBError_FirstHit(t *testing.T) { // Scenario: cache account has empty TempUnschedulableReason, // DB lookup returns nil (not found) → should treat as first hit → temp unscheduled. repo := &dbFallbackRepoStub{ dbAccount: nil, // GetByID returns nil, nil } svc := NewRateLimitService(repo, nil, &config.Config{}, nil, nil) account := &Account{ ID: 22, Type: AccountTypeOAuth, Platform: PlatformAntigravity, TempUnschedulableReason: "", Credentials: map[string]any{ "temp_unschedulable_enabled": true, "temp_unschedulable_rules": []any{ map[string]any{ "error_code": float64(401), "keywords": []any{"unauthorized"}, "duration_minutes": float64(10), }, }, }, } result := svc.CheckErrorPolicy(context.Background(), account, http.StatusUnauthorized, []byte(`unauthorized`)) require.Equal(t, ErrorPolicyTempUnscheduled, result, "401 first hit with DB not found should temp-unschedule") }