Files
huangguo_server/app/service/contentmarkerser/content_marker_test.go
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

230 lines
6.2 KiB
Go

package contentmarkerser
import (
"bytes"
"encoding/json"
"errors"
"sync"
"testing"
"time"
"91porn-server/common/constant/redisconst"
)
type markerCacheStub struct {
data Response
getErr error
setData Response
setCalls int
latestListCached bool
latestListReviewAt *time.Time
}
type markerRaceCache struct {
mu sync.Mutex
payload string
exists bool
}
func (c *markerRaceCache) GetWithJson2Any(_ string, val any) error {
c.mu.Lock()
defer c.mu.Unlock()
if !c.exists {
return errors.New("cache miss")
}
return json.Unmarshal([]byte(c.payload), val)
}
func (c *markerRaceCache) SetNX(_ string, val interface{}, _ time.Duration) (bool, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.exists {
return false, nil
}
payload, ok := val.(string)
if !ok {
return false, errors.New("cache value is not JSON text")
}
c.payload = payload
c.exists = true
return true, nil
}
func (c *markerCacheStub) GetWithJson2Any(key string, val any) error {
if key == redisconst.GetMostNewModuleVideoListKey(
homeLatestListSortType,
homeLatestListPageNumber,
homeLatestListPageSize,
) {
if !c.latestListCached {
return errors.New("latest list cache miss")
}
data := cachedHomeLatestList{}
if c.latestListReviewAt != nil {
data.Videos = append(data.Videos, struct {
ReviewAt time.Time `json:"reviewAt"`
}{ReviewAt: *c.latestListReviewAt})
}
payload, err := json.Marshal(data)
if err != nil {
return err
}
return json.Unmarshal(payload, val)
}
if c.getErr != nil {
return c.getErr
}
payload, err := json.Marshal(c.data)
if err != nil {
return err
}
return json.Unmarshal(payload, val)
}
func (c *markerCacheStub) SetNX(_ string, val interface{}, _ time.Duration) (bool, error) {
c.setCalls++
payload, ok := val.(string)
if !ok {
return false, errors.New("cache value is not JSON text")
}
if err := json.Unmarshal([]byte(payload), &c.setData); err != nil {
return false, err
}
return true, nil
}
func TestResponseOmitsTodayLatestAt(t *testing.T) {
payload, err := json.Marshal(Response{})
if err != nil {
t.Fatalf("json.Marshal(Response{}) error = %v", err)
}
if bytes.Contains(payload, []byte("todayLatestAt")) {
t.Fatalf("response still contains removed todayLatestAt field: %s", payload)
}
}
func TestGetCachedReturnsCachedMarkers(t *testing.T) {
latest := time.Date(2026, 8, 4, 11, 30, 0, 123000000, time.FixedZone("CST", 8*60*60))
cache := &markerCacheStub{data: Response{HomeLatestAt: &latest}}
got, err := GetCached(time.Now(), cache)
if err != nil {
t.Fatalf("GetCached() error = %v", err)
}
if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(latest) {
t.Fatalf("GetCached() homeLatestAt = %v, want %v", got.HomeLatestAt, latest)
}
if cache.setCalls != 0 {
t.Fatalf("GetCached() cache writes = %d, want 0", cache.setCalls)
}
}
func TestReadCachedReportsMiss(t *testing.T) {
cache := &markerCacheStub{getErr: errors.New("cache miss")}
if _, ok := ReadCached(cache); ok {
t.Fatal("ReadCached() hit = true, want false")
}
if _, ok := ReadCached(nil); ok {
t.Fatal("ReadCached(nil) hit = true, want false")
}
}
func TestGetCachedUsesHomeLatestAtFromCachedList(t *testing.T) {
markerLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC)
listLatest := markerLatest.Add(-2 * time.Minute)
cache := &markerCacheStub{
data: Response{HomeLatestAt: &markerLatest},
latestListCached: true,
latestListReviewAt: &listLatest,
}
got, err := GetCached(time.Now(), cache)
if err != nil {
t.Fatalf("GetCached() error = %v", err)
}
if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(listLatest) {
t.Fatalf("GetCached() homeLatestAt = %v, want cached list time %v", got.HomeLatestAt, listLatest)
}
}
func TestGetCachedClearsHomeLatestAtForCachedEmptyList(t *testing.T) {
markerLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC)
cache := &markerCacheStub{
data: Response{HomeLatestAt: &markerLatest},
latestListCached: true,
}
got, err := GetCached(time.Now(), cache)
if err != nil {
t.Fatalf("GetCached() error = %v", err)
}
if got.HomeLatestAt != nil {
t.Fatalf("GetCached() homeLatestAt = %v, want nil for cached empty list", got.HomeLatestAt)
}
}
func TestGetCachedLoadsAndWritesCacheBeforeReturning(t *testing.T) {
latest := time.Date(2026, 8, 4, 11, 31, 0, 0, time.UTC)
cache := &markerCacheStub{getErr: errors.New("cache miss")}
loadCalls := 0
load := func(time.Time) (Response, error) {
loadCalls++
return Response{HomeLatestAt: &latest}, nil
}
got, err := getCached(time.Now(), cache, load)
if err != nil {
t.Fatalf("getCached() error = %v", err)
}
if loadCalls != 1 {
t.Fatalf("getCached() loader calls = %d, want 1", loadCalls)
}
if cache.setCalls != 1 || cache.setData.HomeLatestAt == nil {
t.Fatalf("getCached() cache write = (%d, %v), want one marker write", cache.setCalls, cache.setData.HomeLatestAt)
}
if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(latest) {
t.Fatalf("getCached() homeLatestAt = %v, want %v", got.HomeLatestAt, latest)
}
}
func TestGetCachedConcurrentMissReturnsWinningSnapshot(t *testing.T) {
oldLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC)
newLatest := oldLatest.Add(time.Minute)
cache := &markerRaceCache{}
oldLoaderStarted := make(chan struct{})
releaseOldLoader := make(chan struct{})
type outcome struct {
data Response
err error
}
oldOutcome := make(chan outcome, 1)
go func() {
data, err := getCached(time.Now(), cache, func(time.Time) (Response, error) {
close(oldLoaderStarted)
<-releaseOldLoader
return Response{HomeLatestAt: &oldLatest}, nil
})
oldOutcome <- outcome{data: data, err: err}
}()
<-oldLoaderStarted
newData, err := getCached(time.Now(), cache, func(time.Time) (Response, error) {
return Response{HomeLatestAt: &newLatest}, nil
})
if err != nil {
t.Fatalf("new getCached() error = %v", err)
}
close(releaseOldLoader)
oldResult := <-oldOutcome
if oldResult.err != nil {
t.Fatalf("old getCached() error = %v", oldResult.err)
}
for name, data := range map[string]Response{"new": newData, "old": oldResult.data} {
if data.HomeLatestAt == nil || !data.HomeLatestAt.Equal(newLatest) {
t.Fatalf("%s getCached() homeLatestAt = %v, want winning %v", name, data.HomeLatestAt, newLatest)
}
}
}