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) } }