@@ -0,0 +1,205 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user