package vidser import ( "errors" "fmt" "testing" "time" "91porn-server/common/stderr" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" ) func TestDecideFreeWatchConsume(t *testing.T) { tests := []struct { name string state freeWatchConsumeState wantCan bool wantConsume bool wantRemaining uint64 }{ { name: "eligible video consumes", state: freeWatchConsumeState{watchCount: 3, totalCount: 3}, wantCan: true, wantConsume: true, wantRemaining: 3, }, { name: "already viewed is idempotent", state: freeWatchConsumeState{viewedToday: true, watchCount: 2, totalCount: 3}, wantCan: true, wantRemaining: 2, }, { name: "vip does not consume", state: freeWatchConsumeState{isVIP: true, watchCount: 2, totalCount: 3}, wantCan: true, wantRemaining: 2, }, { name: "publisher does not consume", state: freeWatchConsumeState{isPublisher: true, watchCount: 2, totalCount: 3}, wantCan: true, wantRemaining: 2, }, { name: "free area does not consume", state: freeWatchConsumeState{freeArea: true, watchCount: 2, totalCount: 3}, wantCan: true, wantRemaining: 2, }, { name: "coin video does not consume", state: freeWatchConsumeState{paidVideo: true, watchCount: 2, totalCount: 3}, wantCan: true, wantRemaining: 2, }, { name: "no remaining count denies new video", state: freeWatchConsumeState{watchCount: 0, totalCount: 3}, wantCan: false, }, { name: "count is clamped to configured total", state: freeWatchConsumeState{watchCount: 8, totalCount: 3}, wantCan: true, wantConsume: true, wantRemaining: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := decideFreeWatchConsume(tt.state) if got.isCan != tt.wantCan || got.shouldConsume != tt.wantConsume || got.watchCount != tt.wantRemaining { t.Fatalf("decision = %+v, want isCan=%v shouldConsume=%v watchCount=%d", got, tt.wantCan, tt.wantConsume, tt.wantRemaining) } }) } } func TestFreeWatchConsumeKeyScope(t *testing.T) { oid := primitive.NewObjectID() day := time.Date(2026, 7, 28, 0, 0, 0, 0, time.Local) key := freeWatchConsumeKey(1001, oid, day) if key != freeWatchConsumeKey(1001, oid, day) { t.Fatal("same user, video and day must produce the same consume key") } if key == freeWatchConsumeKey(1002, oid, day) { t.Fatal("consume key must be scoped by user") } if key == freeWatchConsumeKey(1001, primitive.NewObjectID(), day) { t.Fatal("consume key must be scoped by video") } if key == freeWatchConsumeKey(1001, oid, day.AddDate(0, 0, 1)) { t.Fatal("consume key must be scoped by day") } } func TestIsRetryableFreeWatchTransactionError(t *testing.T) { tests := []struct { name string err error want bool }{ { name: "wrapped transient label", err: fmt.Errorf("decrement watch count: %w", mongo.CommandError{ Code: 112, Labels: []string{"TransientTransactionError"}, }), want: true, }, { name: "write conflict code", err: mongo.CommandError{Code: 112}, want: true, }, { name: "write exception transient label", err: mongo.WriteException{ Labels: []string{"TransientTransactionError"}, }, want: true, }, { name: "no such transaction code", err: fmt.Errorf("retry transaction: %w", mongo.CommandError{Code: 251}), want: true, }, { name: "ordinary database error", err: errors.New("database unavailable"), }, { name: "nil", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := isRetryableFreeWatchTransactionError(tt.err); got != tt.want { t.Fatalf("isRetryableFreeWatchTransactionError() = %v, want %v", got, tt.want) } }) } } func TestRunFreeWatchTransactionWithRetry(t *testing.T) { transient := fmt.Errorf("wrapped write conflict: %w", mongo.CommandError{ Code: 112, Labels: []string{"TransientTransactionError"}, }) t.Run("eventually succeeds", func(t *testing.T) { calls := 0 err := runFreeWatchTransactionWithRetry(func() error { calls++ if calls < 3 { return transient } return nil }) if err != nil || calls != 3 { t.Fatalf("err = %v, calls = %d, want nil and 3 calls", err, calls) } }) t.Run("stops at retry limit", func(t *testing.T) { calls := 0 err := runFreeWatchTransactionWithRetry(func() error { calls++ return transient }) if err == nil || calls != freeWatchTransactionRetryLimit { t.Fatalf("err = %v, calls = %d, want error and %d calls", err, calls, freeWatchTransactionRetryLimit) } }) t.Run("duplicate is handled by caller without retry", func(t *testing.T) { calls := 0 err := runFreeWatchTransactionWithRetry(func() error { calls++ return stderr.InsertExistError }) if !stderr.IsEqual(err, stderr.InsertExistError) || calls != 1 { t.Fatalf("err = %v, calls = %d, want duplicate and 1 call", err, calls) } }) t.Run("ordinary error is not retried", func(t *testing.T) { calls := 0 ordinary := errors.New("database unavailable") err := runFreeWatchTransactionWithRetry(func() error { calls++ return ordinary }) if !errors.Is(err, ordinary) || calls != 1 { t.Fatalf("err = %v, calls = %d, want ordinary error and 1 call", err, calls) } }) }