@@ -0,0 +1,322 @@
|
||||
package moduleconfmod
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/commod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
type moduleCacheTestClock struct {
|
||||
mu sync.Mutex
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func (c *moduleCacheTestClock) Now() time.Time {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.now
|
||||
}
|
||||
|
||||
func (c *moduleCacheTestClock) Advance(duration time.Duration) {
|
||||
c.mu.Lock()
|
||||
c.now = c.now.Add(duration)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func TestModuleSnapshotCacheConcurrentReadersShareOneLoad(t *testing.T) {
|
||||
clock := &moduleCacheTestClock{now: time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC)}
|
||||
moduleID := primitive.NewObjectID()
|
||||
var calls atomic.Int32
|
||||
var startedOnce sync.Once
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
cache := newModuleSnapshotCache(func() ([]ModuleConf, error) {
|
||||
calls.Add(1)
|
||||
startedOnce.Do(func() { close(started) })
|
||||
<-release
|
||||
return []ModuleConf{{ID: moduleID, SubModuleName: "cached"}}, nil
|
||||
}, clock.Now, time.Minute, time.Second)
|
||||
|
||||
const readers = 32
|
||||
errs := make(chan error, readers)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(readers)
|
||||
for i := 0; i < readers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
snapshot, err := cache.get()
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
module, ok := snapshot.findByID(moduleID)
|
||||
if !ok || module.SubModuleName != "cached" {
|
||||
errs <- errors.New("reader received an unexpected snapshot")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
<-started
|
||||
close(release)
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
t.Error(err)
|
||||
}
|
||||
if got := calls.Load(); got != 1 {
|
||||
t.Fatalf("loader calls = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleSnapshotCacheInitialFailureReturnsError(t *testing.T) {
|
||||
wantErr := errors.New("mongo unavailable")
|
||||
cache := newModuleSnapshotCache(func() ([]ModuleConf, error) {
|
||||
return nil, wantErr
|
||||
}, time.Now, time.Minute, time.Second)
|
||||
|
||||
if _, err := cache.get(); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("get() error = %v, want %v", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleSnapshotCacheUsesLastKnownGoodAndRetries(t *testing.T) {
|
||||
clock := &moduleCacheTestClock{now: time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC)}
|
||||
moduleID := primitive.NewObjectID()
|
||||
var calls atomic.Int32
|
||||
cache := newModuleSnapshotCache(func() ([]ModuleConf, error) {
|
||||
switch calls.Add(1) {
|
||||
case 1:
|
||||
return []ModuleConf{{ID: moduleID, SubModuleName: "v1"}}, nil
|
||||
case 2:
|
||||
return nil, errors.New("temporary mongo failure")
|
||||
default:
|
||||
return []ModuleConf{{ID: moduleID, SubModuleName: "v2"}}, nil
|
||||
}
|
||||
}, clock.Now, 10*time.Second, time.Second)
|
||||
|
||||
assertCachedModuleName(t, cache, moduleID, "v1")
|
||||
clock.Advance(11 * time.Second)
|
||||
assertCachedModuleName(t, cache, moduleID, "v1")
|
||||
assertCachedModuleName(t, cache, moduleID, "v1")
|
||||
if got := calls.Load(); got != 2 {
|
||||
t.Fatalf("loader calls during retry delay = %d, want 2", got)
|
||||
}
|
||||
|
||||
clock.Advance(2 * time.Second)
|
||||
assertCachedModuleName(t, cache, moduleID, "v2")
|
||||
if got := calls.Load(); got != 3 {
|
||||
t.Fatalf("loader calls after retry delay = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleSnapshotCacheInvalidateForcesReload(t *testing.T) {
|
||||
moduleID := primitive.NewObjectID()
|
||||
var calls atomic.Int32
|
||||
cache := newModuleSnapshotCache(func() ([]ModuleConf, error) {
|
||||
version := calls.Add(1)
|
||||
return []ModuleConf{{
|
||||
ID: moduleID,
|
||||
SubModuleName: "v" + string(rune('0'+version)),
|
||||
}}, nil
|
||||
}, time.Now, time.Hour, time.Second)
|
||||
|
||||
assertCachedModuleName(t, cache, moduleID, "v1")
|
||||
cache.invalidate()
|
||||
assertCachedModuleName(t, cache, moduleID, "v2")
|
||||
if got := calls.Load(); got != 2 {
|
||||
t.Fatalf("loader calls = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleSnapshotCacheInvalidateDuringLoadDiscardsStaleResult(t *testing.T) {
|
||||
moduleID := primitive.NewObjectID()
|
||||
var calls atomic.Int32
|
||||
firstStarted := make(chan struct{})
|
||||
releaseFirst := make(chan struct{})
|
||||
cache := newModuleSnapshotCache(func() ([]ModuleConf, error) {
|
||||
call := calls.Add(1)
|
||||
if call == 1 {
|
||||
close(firstStarted)
|
||||
<-releaseFirst
|
||||
return []ModuleConf{{ID: moduleID, SubModuleName: "stale"}}, nil
|
||||
}
|
||||
return []ModuleConf{{ID: moduleID, SubModuleName: "fresh"}}, nil
|
||||
}, time.Now, time.Hour, time.Second)
|
||||
|
||||
result := make(chan string, 1)
|
||||
errs := make(chan error, 1)
|
||||
go func() {
|
||||
snapshot, err := cache.get()
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
module, _ := snapshot.findByID(moduleID)
|
||||
result <- module.SubModuleName
|
||||
}()
|
||||
|
||||
<-firstStarted
|
||||
cache.invalidate()
|
||||
close(releaseFirst)
|
||||
|
||||
select {
|
||||
case err := <-errs:
|
||||
t.Fatal(err)
|
||||
case got := <-result:
|
||||
if got != "fresh" {
|
||||
t.Fatalf("module name = %q, want fresh", got)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("cache reload timed out")
|
||||
}
|
||||
if got := calls.Load(); got != 2 {
|
||||
t.Fatalf("loader calls = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleSnapshotScheduleBoundariesDoNotRequireReload(t *testing.T) {
|
||||
base := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC)
|
||||
onlineAt := base.Add(time.Hour)
|
||||
offlineAt := base.Add(2 * time.Hour)
|
||||
moduleID := primitive.NewObjectID()
|
||||
var calls atomic.Int32
|
||||
cache := newModuleSnapshotCache(func() ([]ModuleConf, error) {
|
||||
calls.Add(1)
|
||||
return []ModuleConf{{
|
||||
ID: moduleID,
|
||||
Status: 1,
|
||||
OnlineAt: &onlineAt,
|
||||
OfflineAt: &offlineAt,
|
||||
SearchOnlyWhenInactive: true,
|
||||
}}, nil
|
||||
}, func() time.Time { return base }, time.Hour, time.Second)
|
||||
|
||||
snapshot, err := cache.get()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertStringSetContains(t, snapshot.excludedVideoModuleIDs(onlineAt.Add(-time.Nanosecond), false), moduleID.Hex(), true)
|
||||
assertStringSetContains(t, snapshot.excludedVideoModuleIDs(onlineAt, false), moduleID.Hex(), false)
|
||||
assertStringSetContains(t, snapshot.excludedVideoModuleIDs(offlineAt, false), moduleID.Hex(), true)
|
||||
if _, ok := snapshot.blockedOutsideSearchModuleIDs([]primitive.ObjectID{moduleID}, onlineAt.Add(-time.Nanosecond))[moduleID.Hex()]; !ok {
|
||||
t.Fatal("module should be blocked before onlineAt")
|
||||
}
|
||||
if _, ok := snapshot.blockedOutsideSearchModuleIDs([]primitive.ObjectID{moduleID}, onlineAt)[moduleID.Hex()]; ok {
|
||||
t.Fatal("module should be browsable at onlineAt")
|
||||
}
|
||||
if _, ok := snapshot.blockedOutsideSearchModuleIDs([]primitive.ObjectID{moduleID}, offlineAt)[moduleID.Hex()]; !ok {
|
||||
t.Fatal("module should be blocked at offlineAt")
|
||||
}
|
||||
|
||||
if got := len(snapshot.activeAt(onlineAt.Add(-time.Nanosecond))); got != 0 {
|
||||
t.Fatalf("active before onlineAt = %d, want 0", got)
|
||||
}
|
||||
if got := len(snapshot.activeAt(onlineAt)); got != 1 {
|
||||
t.Fatalf("active at onlineAt = %d, want 1", got)
|
||||
}
|
||||
if got := len(snapshot.activeAt(offlineAt)); got != 0 {
|
||||
t.Fatalf("active at offlineAt = %d, want 0", got)
|
||||
}
|
||||
if got := calls.Load(); got != 1 {
|
||||
t.Fatalf("loader calls = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleSnapshotExcludedSearchModuleIDs(t *testing.T) {
|
||||
excludedID := primitive.NewObjectID()
|
||||
includedID := primitive.NewObjectID()
|
||||
snapshot := newModuleSnapshot([]ModuleConf{
|
||||
{ID: excludedID, ExcludeSearch: true},
|
||||
{ID: includedID, ExcludeSearch: false},
|
||||
})
|
||||
|
||||
ids := snapshot.excludedSearchModuleIDs()
|
||||
assertStringSetContains(t, ids, excludedID.Hex(), true)
|
||||
assertStringSetContains(t, ids, includedID.Hex(), false)
|
||||
}
|
||||
|
||||
func TestModuleSnapshotAccessorsReturnDeepCopies(t *testing.T) {
|
||||
moduleID := primitive.NewObjectID()
|
||||
onlineAt := time.Date(2026, 7, 31, 8, 0, 0, 0, time.UTC)
|
||||
snapshot := newModuleSnapshot([]ModuleConf{{
|
||||
ID: moduleID,
|
||||
SubModuleName: "original",
|
||||
OnlineAt: &onlineAt,
|
||||
HaiJiaoStyle: HaiJiaoStyle{
|
||||
SortRules: []SortItem{{Val: commod.MostHot, Name: "original-rule"}},
|
||||
},
|
||||
}})
|
||||
|
||||
first := snapshot.all()
|
||||
first[0].SubModuleName = "changed"
|
||||
*first[0].OnlineAt = first[0].OnlineAt.Add(time.Hour)
|
||||
first[0].HaiJiaoStyle.SortRules[0].Name = "changed-rule"
|
||||
|
||||
second := snapshot.all()
|
||||
if second[0].SubModuleName != "original" {
|
||||
t.Fatalf("shared scalar was mutated: %+v", second[0])
|
||||
}
|
||||
if !second[0].OnlineAt.Equal(onlineAt) {
|
||||
t.Fatalf("shared schedule was mutated: %v", second[0].OnlineAt)
|
||||
}
|
||||
if second[0].HaiJiaoStyle.SortRules[0].Name != "original-rule" {
|
||||
t.Fatalf("shared sort rules were mutated: %+v", second[0].HaiJiaoStyle.SortRules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetByIDPreservesNoDocumentsContract(t *testing.T) {
|
||||
original := moduleMetadataCache
|
||||
moduleMetadataCache = newModuleSnapshotCache(
|
||||
func() ([]ModuleConf, error) { return []ModuleConf{}, nil },
|
||||
time.Now,
|
||||
time.Hour,
|
||||
time.Second,
|
||||
)
|
||||
t.Cleanup(func() { moduleMetadataCache = original })
|
||||
|
||||
if _, err := GetByID(primitive.NewObjectID()); !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
t.Fatalf("GetByID() error = %v, want mongo.ErrNoDocuments", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertCachedModuleName(
|
||||
t *testing.T,
|
||||
cache *moduleSnapshotCache,
|
||||
moduleID primitive.ObjectID,
|
||||
want string,
|
||||
) {
|
||||
t.Helper()
|
||||
snapshot, err := cache.get()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
module, ok := snapshot.findByID(moduleID)
|
||||
if !ok {
|
||||
t.Fatalf("module %s not found", moduleID.Hex())
|
||||
}
|
||||
if module.SubModuleName != want {
|
||||
t.Fatalf("module name = %q, want %q", module.SubModuleName, want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertStringSetContains(t *testing.T, values []string, target string, want bool) {
|
||||
t.Helper()
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
if !want {
|
||||
t.Fatalf("%q unexpectedly found in %v", target, values)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if want {
|
||||
t.Fatalf("%q not found in %v", target, values)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user