add timeline service
This commit is contained in:
parent
c9a0926495
commit
d82f5c3337
6
Makefile
6
Makefile
|
@ -51,9 +51,9 @@ gen-mongo-model: # 建立 rpc 資料庫
|
|||
# 只產生 Model 剩下的要自己撰寫,連欄位名稱也是
|
||||
goctl model mongo -t post --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||
goctl model mongo -t comment --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||
# goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||
# goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||
# goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||
goctl model mongo -t tags --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||
goctl model mongo -t post_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||
goctl model mongo -t comment_likes --dir ./internal/model/mongo --style $(GO_ZERO_STYLE)
|
||||
@echo "Generate mongo model files successfully"
|
||||
|
||||
.PHONY: mock-gen
|
||||
|
|
|
@ -12,3 +12,7 @@ Mongo:
|
|||
Password: ""
|
||||
Port: "27017"
|
||||
Database: digimon_tweeting
|
||||
|
||||
TimelineSetting:
|
||||
Expire: 86400
|
||||
MaxLength: 1000
|
|
@ -201,8 +201,17 @@ message GetTimelineResp
|
|||
message AddPostToTimelineReq
|
||||
{
|
||||
string uid = 1; // key
|
||||
repeated PostDetailItem posts = 2;
|
||||
repeated PostTimelineItem posts = 3;
|
||||
}
|
||||
|
||||
// 貼文更新
|
||||
message PostTimelineItem
|
||||
{
|
||||
string post_id = 1; // 貼文ID
|
||||
int64 created_at = 7; // 發佈時間 -> 排序使用
|
||||
}
|
||||
|
||||
|
||||
// TimelineService 業務邏輯在外面組合
|
||||
service TimelineService
|
||||
{
|
||||
|
|
3
go.mod
3
go.mod
|
@ -5,6 +5,7 @@ go 1.22.3
|
|||
require (
|
||||
code.30cm.net/digimon/library-go/errs v1.2.4
|
||||
code.30cm.net/digimon/library-go/validator v1.0.0
|
||||
github.com/alicebob/miniredis/v2 v2.33.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/zeromicro/go-zero v1.7.0
|
||||
go.mongodb.org/mongo-driver v1.16.0
|
||||
|
@ -14,6 +15,7 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
|
@ -65,6 +67,7 @@ require (
|
|||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.15 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.15 // indirect
|
||||
|
|
|
@ -4,6 +4,7 @@ import "github.com/zeromicro/go-zero/zrpc"
|
|||
|
||||
type Config struct {
|
||||
zrpc.RpcServerConf
|
||||
|
||||
Mongo struct {
|
||||
Schema string
|
||||
User string
|
||||
|
@ -12,4 +13,9 @@ type Config struct {
|
|||
Port string
|
||||
Database string
|
||||
}
|
||||
|
||||
TimelineSetting struct {
|
||||
Expire int64 // Second
|
||||
MaxLength int64 // 暫存筆數
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,3 +11,7 @@ const (
|
|||
AdTypeOnlyAd
|
||||
AdTypeOnlyNotAd
|
||||
)
|
||||
|
||||
const (
|
||||
LastOfTimelineFlag = "NoMoreData"
|
||||
)
|
||||
|
|
|
@ -33,6 +33,10 @@ const (
|
|||
CommentListErrorCode
|
||||
)
|
||||
|
||||
const (
|
||||
TimeLineErrorCode ErrorCode = iota + 20
|
||||
)
|
||||
|
||||
func CommentError(ec ErrorCode, s ...string) *ers.LibError {
|
||||
return ers.NewError(code.CloudEPTweeting, code.DBError,
|
||||
ec.ToUint32(),
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package domain
|
||||
|
||||
import "strings"
|
||||
|
||||
type RedisKey string
|
||||
|
||||
func (key RedisKey) ToString() string {
|
||||
return string(key)
|
||||
}
|
||||
|
||||
func (key RedisKey) With(s ...string) RedisKey {
|
||||
parts := append([]string{string(key)}, s...)
|
||||
|
||||
return RedisKey(strings.Join(parts, ":"))
|
||||
}
|
||||
|
||||
const (
|
||||
TimelineRedisKey RedisKey = "timeline"
|
||||
)
|
|
@ -0,0 +1,45 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestRedisKeyToString 測試 ToString 方法
|
||||
func TestRedisKeyToString(t *testing.T) {
|
||||
key := RedisKey("user:timeline")
|
||||
expected := "user:timeline"
|
||||
result := key.ToString()
|
||||
|
||||
assert.Equal(t, expected, result, "ToString should return the correct string representation of RedisKey")
|
||||
}
|
||||
|
||||
// TestRedisKeyWith 測試 With 方法
|
||||
func TestRedisKeyWith(t *testing.T) {
|
||||
key := RedisKey("user:timeline")
|
||||
subKey := "12345"
|
||||
expected := "user:timeline:12345"
|
||||
result := key.With(subKey)
|
||||
|
||||
assert.Equal(t, RedisKey(expected), result, "With should correctly concatenate the RedisKey with the provided subKey")
|
||||
}
|
||||
|
||||
// TestRedisKeyWithMultiple 測試 With 方法與多個參數
|
||||
func TestRedisKeyWithMultiple(t *testing.T) {
|
||||
key := RedisKey("user:timeline")
|
||||
subKeys := []string{"12345", "posts"}
|
||||
expected := "user:timeline:12345:posts"
|
||||
result := key.With(subKeys...)
|
||||
|
||||
assert.Equal(t, RedisKey(expected), result, "With should correctly concatenate the RedisKey with multiple provided subKeys")
|
||||
}
|
||||
|
||||
// TestRedisKeyWithEmpty 測試 With 方法與空參數
|
||||
func TestRedisKeyWithEmpty(t *testing.T) {
|
||||
key := RedisKey("user:timeline")
|
||||
expected := "user:timeline"
|
||||
result := key.With()
|
||||
|
||||
assert.Equal(t, RedisKey(expected), result, "With should return the original key when no subKeys are provided")
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||
"context"
|
||||
)
|
||||
|
||||
/*
|
||||
----------------------------------------
|
||||
| | | | |
|
||||
| data A| data B |NO Data | .... |
|
||||
| | | | |
|
||||
----------------------------------------
|
||||
|
||||
動態時報在發現這個 Queue 有 No Data 的 Flag 時
|
||||
就不再去 Query 資料庫,防止被狂刷。
|
||||
只是要注意在業務上何時要 加入/刪除 這個 Flag
|
||||
*/
|
||||
|
||||
// TimelineRepository 定義時間線的存儲接口,可以根據不同的排序策略實現。
|
||||
type TimelineRepository interface {
|
||||
// AddPost 將貼文添加到動態時報,並根據排序策略進行排序。
|
||||
AddPost(ctx context.Context, req AddPostRequest) error
|
||||
// FetchTimeline 獲取指定用戶的動態時報。
|
||||
FetchTimeline(ctx context.Context, req FetchTimelineRequest) (FetchTimelineResponse, error)
|
||||
// SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫。
|
||||
SetNoMoreDataFlag(ctx context.Context, uid string) error
|
||||
// HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫。
|
||||
HasNoMoreData(ctx context.Context, uid string) (bool, error)
|
||||
// ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌。
|
||||
ClearNoMoreDataFlag(ctx context.Context, uid string) error
|
||||
}
|
||||
|
||||
// AddPostRequest 用於將貼文添加到時間線的請求結構體。
|
||||
type AddPostRequest struct {
|
||||
UID string
|
||||
PostItems []TimelineItem
|
||||
}
|
||||
|
||||
// TimelineItem 表示時間線中的一個元素,排序依據取決於 Score。
|
||||
type TimelineItem struct {
|
||||
PostID string // 貼文ID
|
||||
Score int64 // 排序使用的分數,根據具體實現可能代表時間、優先級等
|
||||
}
|
||||
|
||||
// FetchTimelineRequest 用於獲取時間線的請求結構體。
|
||||
type FetchTimelineRequest struct {
|
||||
UID string
|
||||
PageSize int64
|
||||
PageIndex int64
|
||||
}
|
||||
|
||||
// FetchTimelineResponse 表示獲取時間線的回應結構體。
|
||||
type FetchTimelineResponse struct {
|
||||
Items []TimelineItem
|
||||
Page tweeting.Pager
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||
"app-cloudep-tweeting-service/internal/config"
|
||||
"app-cloudep-tweeting-service/internal/domain"
|
||||
"app-cloudep-tweeting-service/internal/domain/repository"
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
)
|
||||
|
||||
// TODO 第一版本先使用 Redis 來做,後續如果有效能考量,在考慮使用其他方案
|
||||
|
||||
type TimelineRepositoryParam struct {
|
||||
Config config.Config
|
||||
Redis redis.Redis
|
||||
}
|
||||
|
||||
type TimelineRepository struct {
|
||||
cfg config.Config
|
||||
redis redis.Redis
|
||||
}
|
||||
|
||||
func MustGenerateUseCase(param TimelineRepositoryParam) repository.TimelineRepository {
|
||||
return &TimelineRepository{
|
||||
cfg: param.Config,
|
||||
redis: param.Redis,
|
||||
}
|
||||
}
|
||||
|
||||
// AddPost 將貼文添加到時間線,並根據 Score 排序
|
||||
func (t *TimelineRepository) AddPost(ctx context.Context, req repository.AddPostRequest) error {
|
||||
key := domain.TimelineRedisKey.With(req.UID).ToString()
|
||||
|
||||
// 準備要插入的元素
|
||||
zItems := make([]redis.Pair, len(req.PostItems))
|
||||
for i, item := range req.PostItems {
|
||||
zItems[i] = redis.Pair{
|
||||
Score: item.Score,
|
||||
Key: item.PostID,
|
||||
}
|
||||
}
|
||||
|
||||
// 將 ZSet 元素添加到 Redis
|
||||
_, err := t.redis.ZaddsCtx(ctx, key, zItems...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 檢查 ZSet 長度,並在超過 maxLength 時刪除多餘的元素
|
||||
if t.cfg.TimelineSetting.MaxLength > 0 {
|
||||
// 這裡從 0 到 - (maxLength+1) 代表超過限制的元素範圍
|
||||
_, err := t.redis.ZremrangebyrankCtx(ctx, key, 0, -(t.cfg.TimelineSetting.MaxLength + 1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 設置過期時間
|
||||
return t.redis.ExpireCtx(ctx, key, int(t.cfg.TimelineSetting.Expire))
|
||||
}
|
||||
|
||||
// FetchTimeline 獲取指定用戶的動態時報
|
||||
func (t *TimelineRepository) FetchTimeline(ctx context.Context, req repository.FetchTimelineRequest) (repository.FetchTimelineResponse, error) {
|
||||
key := domain.TimelineRedisKey.With(req.UID).ToString()
|
||||
|
||||
start := (req.PageIndex - 1) * req.PageSize
|
||||
end := start + req.PageSize - 1
|
||||
|
||||
// 從 Redis 中按分數由高到低獲取時間線元素
|
||||
pair, err := t.redis.ZrevrangeWithScoresCtx(ctx, key, start, end)
|
||||
if err != nil {
|
||||
return repository.FetchTimelineResponse{}, err
|
||||
}
|
||||
|
||||
// 構建返回結果
|
||||
items := make([]repository.TimelineItem, len(pair))
|
||||
for i, z := range pair {
|
||||
items[i] = repository.TimelineItem{
|
||||
PostID: z.Key,
|
||||
Score: z.Score,
|
||||
}
|
||||
}
|
||||
|
||||
// 計算總數量
|
||||
total, err := t.redis.ZcardCtx(ctx, key)
|
||||
if err != nil {
|
||||
return repository.FetchTimelineResponse{}, err
|
||||
}
|
||||
|
||||
return repository.FetchTimelineResponse{
|
||||
Items: items,
|
||||
Page: tweeting.Pager{
|
||||
Total: int64(total),
|
||||
Index: req.PageIndex,
|
||||
Size: req.PageSize,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetNoMoreDataFlag 標記時間線已完整,避免繼續查詢資料庫
|
||||
func (t *TimelineRepository) SetNoMoreDataFlag(ctx context.Context, uid string) error {
|
||||
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||
|
||||
// 添加一個標誌到時間線的 ZSet
|
||||
_, err := t.redis.ZaddsCtx(ctx, key, redis.Pair{
|
||||
Score: time.Now().UTC().Unix(),
|
||||
Key: domain.LastOfTimelineFlag,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 設置過期時間
|
||||
return t.redis.ExpireCtx(ctx, key, int(t.cfg.TimelineSetting.Expire))
|
||||
}
|
||||
|
||||
// HasNoMoreData 檢查時間線是否已完整,決定是否需要查詢資料庫
|
||||
func (t *TimelineRepository) HasNoMoreData(ctx context.Context, uid string) (bool, error) {
|
||||
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||
|
||||
// 檢查 "NoMoreData" 標誌是否存在
|
||||
score, err := t.redis.ZscoreCtx(ctx, key, domain.LastOfTimelineFlag)
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return false, nil // 標誌不存在
|
||||
}
|
||||
if err != nil {
|
||||
return false, err // 其他錯誤
|
||||
}
|
||||
|
||||
return score != 0, nil
|
||||
}
|
||||
|
||||
// ClearNoMoreDataFlag 清除時間線的 "NoMoreData" 標誌
|
||||
func (t *TimelineRepository) ClearNoMoreDataFlag(ctx context.Context, uid string) error {
|
||||
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||
// 移除 "NoMoreData" 標誌
|
||||
_, err := t.redis.ZremCtx(ctx, key, domain.LastOfTimelineFlag)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,439 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"app-cloudep-tweeting-service/internal/config"
|
||||
"app-cloudep-tweeting-service/internal/domain"
|
||||
"app-cloudep-tweeting-service/internal/domain/repository"
|
||||
"context"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewRepo() (*miniredis.Miniredis, repository.TimelineRepository, error) {
|
||||
r1, err := miniredis.Run()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
newRedis, err := redis.NewRedis(redis.RedisConf{
|
||||
Host: r1.Addr(),
|
||||
Type: redis.ClusterType,
|
||||
Pass: "",
|
||||
})
|
||||
if err != nil {
|
||||
r1.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
c := config.Config{
|
||||
TimelineSetting: struct {
|
||||
Expire int64
|
||||
MaxLength int64
|
||||
}{Expire: 86400, MaxLength: 1000},
|
||||
}
|
||||
|
||||
timelineRepo := MustGenerateUseCase(TimelineRepositoryParam{
|
||||
Config: c,
|
||||
Redis: *newRedis,
|
||||
})
|
||||
|
||||
return r1, timelineRepo, nil
|
||||
}
|
||||
|
||||
func TestAddPost(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action func(t *testing.T, repo repository.TimelineRepository) error
|
||||
expectErr bool
|
||||
validate func(t *testing.T, r1 *miniredis.Miniredis)
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) error {
|
||||
ctx := context.Background()
|
||||
uid := "OOOOOOKJ"
|
||||
|
||||
return repo.AddPost(ctx, repository.AddPostRequest{
|
||||
UID: uid,
|
||||
PostItems: []repository.TimelineItem{
|
||||
{PostID: "post1", Score: 100},
|
||||
},
|
||||
})
|
||||
},
|
||||
expectErr: false,
|
||||
validate: func(t *testing.T, r1 *miniredis.Miniredis) {
|
||||
uid := "OOOOOOKJ"
|
||||
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||
score, err := r1.ZScore(key, "post1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, float64(100), score)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "timeout",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) error {
|
||||
ctx := context.Background()
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Millisecond)
|
||||
defer cancel()
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
uid := "OOOOOLK"
|
||||
return repo.AddPost(timeoutCtx, repository.AddPostRequest{
|
||||
UID: uid,
|
||||
PostItems: []repository.TimelineItem{
|
||||
{PostID: "post2", Score: 200},
|
||||
},
|
||||
})
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Redis error on Zadd",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) error {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
r1.Close() // 模拟 Redis 错误
|
||||
|
||||
ctx := context.Background()
|
||||
uid := "OOOOOWE"
|
||||
return repo.AddPost(ctx, repository.AddPostRequest{
|
||||
UID: uid,
|
||||
PostItems: []repository.TimelineItem{
|
||||
{PostID: "post3", Score: 300},
|
||||
},
|
||||
})
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
defer r1.Close()
|
||||
|
||||
err = tt.action(t, repo)
|
||||
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tt.validate != nil {
|
||||
tt.validate(t, r1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchTimeline(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error)
|
||||
expectErr bool
|
||||
validate func(t *testing.T, r1 *miniredis.Miniredis, resp *repository.FetchTimelineResponse)
|
||||
}{
|
||||
{
|
||||
name: "FetchTimeline - success",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) {
|
||||
ctx := context.Background()
|
||||
uid := "user123"
|
||||
|
||||
_ = repo.AddPost(ctx, repository.AddPostRequest{
|
||||
UID: uid,
|
||||
PostItems: []repository.TimelineItem{
|
||||
{PostID: "post1", Score: 200},
|
||||
{PostID: "post2", Score: 100},
|
||||
},
|
||||
})
|
||||
|
||||
return repo.FetchTimeline(ctx, repository.FetchTimelineRequest{
|
||||
UID: uid,
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
})
|
||||
},
|
||||
expectErr: false,
|
||||
validate: func(t *testing.T, r1 *miniredis.Miniredis, resp *repository.FetchTimelineResponse) {
|
||||
assert.Equal(t, 2, len(resp.Items))
|
||||
assert.Equal(t, "post1", resp.Items[0].PostID)
|
||||
assert.Equal(t, "post2", resp.Items[1].PostID)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FetchTimeline - timeout",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) {
|
||||
ctx := context.Background()
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 1*time.Millisecond)
|
||||
defer cancel()
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
uid := "user123"
|
||||
return repo.FetchTimeline(timeoutCtx, repository.FetchTimelineRequest{
|
||||
UID: uid,
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
})
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "FetchTimeline - Redis error on ZrangebyscoreWithScoresCtx",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
defer r1.Close()
|
||||
|
||||
uid := "user123"
|
||||
|
||||
_ = repo.AddPost(context.Background(), repository.AddPostRequest{
|
||||
UID: uid,
|
||||
PostItems: []repository.TimelineItem{
|
||||
{PostID: "post3", Score: 300},
|
||||
},
|
||||
})
|
||||
|
||||
r1.Close()
|
||||
|
||||
return repo.FetchTimeline(context.Background(), repository.FetchTimelineRequest{
|
||||
UID: uid,
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
})
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "FetchTimeline - Redis error on ZcardCtx",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) (repository.FetchTimelineResponse, error) {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
defer r1.Close()
|
||||
|
||||
uid := "user123"
|
||||
|
||||
_ = repo.AddPost(context.Background(), repository.AddPostRequest{
|
||||
UID: uid,
|
||||
PostItems: []repository.TimelineItem{
|
||||
{PostID: "post4", Score: 400},
|
||||
},
|
||||
})
|
||||
|
||||
r1.Close()
|
||||
|
||||
return repo.FetchTimeline(context.Background(), repository.FetchTimelineRequest{
|
||||
UID: uid,
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
})
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
defer r1.Close()
|
||||
|
||||
resp, err := tt.action(t, repo)
|
||||
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tt.validate != nil {
|
||||
tt.validate(t, r1, &resp)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNoMoreDataFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action func(t *testing.T, repo repository.TimelineRepository) error
|
||||
expectErr bool
|
||||
validate func(t *testing.T, r1 *miniredis.Miniredis)
|
||||
}{
|
||||
{
|
||||
name: "SetNoMoreDataFlag - success",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) error {
|
||||
ctx := context.Background()
|
||||
uid := "user123"
|
||||
return repo.SetNoMoreDataFlag(ctx, uid)
|
||||
},
|
||||
expectErr: false,
|
||||
validate: func(t *testing.T, r1 *miniredis.Miniredis) {
|
||||
uid := "user123"
|
||||
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||
score, _ := r1.ZScore(key, domain.LastOfTimelineFlag)
|
||||
assert.NotZero(t, score)
|
||||
|
||||
// 驗證是否設定過期時間
|
||||
ttl := r1.TTL(key)
|
||||
assert.Equal(t, time.Duration(86400)*time.Second, ttl)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SetNoMoreDataFlag - Redis error on ZaddsCtx",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) error {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
r1.Close() // 手動關閉,復現錯誤
|
||||
|
||||
ctx := context.Background()
|
||||
uid := "user123"
|
||||
return repo.SetNoMoreDataFlag(ctx, uid)
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "SetNoMoreDataFlag - Redis error on ExpireCtx",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) error {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
uid := "user123"
|
||||
|
||||
r1.Close()
|
||||
|
||||
return repo.SetNoMoreDataFlag(ctx, uid)
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
defer r1.Close()
|
||||
|
||||
err = tt.action(t, repo)
|
||||
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tt.validate != nil {
|
||||
tt.validate(t, r1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasNoMoreData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action func(t *testing.T, repo repository.TimelineRepository) (bool, error)
|
||||
expectErr bool
|
||||
expected bool
|
||||
setup func(r1 *miniredis.Miniredis)
|
||||
}{
|
||||
{
|
||||
name: "HasNoMoreData - 標誌存在",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) (bool, error) {
|
||||
ctx := context.Background()
|
||||
uid := "user123"
|
||||
return repo.HasNoMoreData(ctx, uid)
|
||||
},
|
||||
expectErr: false,
|
||||
expected: true,
|
||||
setup: func(r1 *miniredis.Miniredis) {
|
||||
uid := "user123"
|
||||
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||
_, _ = r1.ZAdd(key, float64(time.Now().UTC().Unix()), domain.LastOfTimelineFlag)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HasNoMoreData - 標誌不存在",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) (bool, error) {
|
||||
ctx := context.Background()
|
||||
uid := "user123"
|
||||
return repo.HasNoMoreData(ctx, uid)
|
||||
},
|
||||
expectErr: false,
|
||||
expected: false,
|
||||
setup: func(r1 *miniredis.Miniredis) {}, // 不設置標誌
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
defer r1.Close()
|
||||
|
||||
tt.setup(r1)
|
||||
|
||||
result, err := tt.action(t, repo)
|
||||
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearNoMoreDataFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action func(t *testing.T, repo repository.TimelineRepository) error
|
||||
expectErr bool
|
||||
setup func(r1 *miniredis.Miniredis)
|
||||
validate func(t *testing.T, r1 *miniredis.Miniredis)
|
||||
}{
|
||||
{
|
||||
name: "ClearNoMoreDataFlag - 成功清除標誌",
|
||||
action: func(t *testing.T, repo repository.TimelineRepository) error {
|
||||
ctx := context.Background()
|
||||
uid := "user123"
|
||||
return repo.ClearNoMoreDataFlag(ctx, uid)
|
||||
},
|
||||
expectErr: false,
|
||||
setup: func(r1 *miniredis.Miniredis) {
|
||||
uid := "user123"
|
||||
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||
_, err := r1.ZAdd(key, 100, domain.LastOfTimelineFlag) // 設置標誌
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
validate: func(t *testing.T, r1 *miniredis.Miniredis) {
|
||||
uid := "user123"
|
||||
key := domain.TimelineRedisKey.With(uid).ToString()
|
||||
_, err := r1.ZScore(key, domain.LastOfTimelineFlag)
|
||||
assert.Error(t, err) // 標誌應該已被移除
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r1, repo, err := NewRepo()
|
||||
assert.NoError(t, err)
|
||||
defer r1.Close()
|
||||
|
||||
tt.setup(r1)
|
||||
|
||||
err = tt.action(t, repo)
|
||||
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
tt.validate(t, r1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import (
|
|||
"context"
|
||||
|
||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||
"app-cloudep-tweeting-service/internal/logic/commentservice"
|
||||
commentservicelogic "app-cloudep-tweeting-service/internal/logic/commentservice"
|
||||
"app-cloudep-tweeting-service/internal/svc"
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"context"
|
||||
|
||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||
"app-cloudep-tweeting-service/internal/logic/postservice"
|
||||
postservicelogic "app-cloudep-tweeting-service/internal/logic/postservice"
|
||||
"app-cloudep-tweeting-service/internal/svc"
|
||||
)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"context"
|
||||
|
||||
"app-cloudep-tweeting-service/gen_result/pb/tweeting"
|
||||
"app-cloudep-tweeting-service/internal/logic/timelineservice"
|
||||
timelineservicelogic "app-cloudep-tweeting-service/internal/logic/timelineservice"
|
||||
"app-cloudep-tweeting-service/internal/svc"
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue