206 lines
5.4 KiB
Go
206 lines
5.4 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|