230 lines
6.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|