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