From d82f5c33370c8f4766f1a0b51800cd235872e076 Mon Sep 17 00:00:00 2001 From: "daniel.w" Date: Sun, 1 Sep 2024 21:49:28 +0800 Subject: [PATCH] add timeline service --- Makefile | 6 +- etc/tweeting.yaml | 6 +- generate/protobuf/tweeting.proto | 11 +- go.mod | 3 + internal/config/config.go | 6 + internal/domain/const.go | 4 + internal/domain/errors.go | 4 + internal/domain/redis.go | 19 + internal/domain/redis_test.go | 45 ++ internal/domain/repository/timeline.go | 57 +++ .../repository/timeline_sort_by_timestamp.go | 143 ++++++ .../timeline_sort_by_timestamp_test.go | 439 ++++++++++++++++++ .../commentservice/comment_service_server.go | 2 +- .../server/postservice/post_service_server.go | 2 +- .../timeline_service_server.go | 2 +- 15 files changed, 741 insertions(+), 8 deletions(-) create mode 100644 internal/domain/redis.go create mode 100644 internal/domain/redis_test.go create mode 100644 internal/domain/repository/timeline.go create mode 100644 internal/repository/timeline_sort_by_timestamp.go create mode 100644 internal/repository/timeline_sort_by_timestamp_test.go diff --git a/Makefile b/Makefile index 20dc350..035a97b 100644 --- a/Makefile +++ b/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 diff --git a/etc/tweeting.yaml b/etc/tweeting.yaml index 279f3cb..0ae0dc7 100644 --- a/etc/tweeting.yaml +++ b/etc/tweeting.yaml @@ -11,4 +11,8 @@ Mongo: User: "" Password: "" Port: "27017" - Database: digimon_tweeting \ No newline at end of file + Database: digimon_tweeting + +TimelineSetting: + Expire: 86400 + MaxLength: 1000 \ No newline at end of file diff --git a/generate/protobuf/tweeting.proto b/generate/protobuf/tweeting.proto index b850ac2..4b8912b 100644 --- a/generate/protobuf/tweeting.proto +++ b/generate/protobuf/tweeting.proto @@ -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 { diff --git a/go.mod b/go.mod index ea41a6a..4fad66b 100644 --- a/go.mod +++ b/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 diff --git a/internal/config/config.go b/internal/config/config.go index 73cf966..ac5c588 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 // 暫存筆數 + } } diff --git a/internal/domain/const.go b/internal/domain/const.go index 0e7211b..060080a 100644 --- a/internal/domain/const.go +++ b/internal/domain/const.go @@ -11,3 +11,7 @@ const ( AdTypeOnlyAd AdTypeOnlyNotAd ) + +const ( + LastOfTimelineFlag = "NoMoreData" +) diff --git a/internal/domain/errors.go b/internal/domain/errors.go index d16b1ab..8a44bb8 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -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(), diff --git a/internal/domain/redis.go b/internal/domain/redis.go new file mode 100644 index 0000000..b8ab2a0 --- /dev/null +++ b/internal/domain/redis.go @@ -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" +) diff --git a/internal/domain/redis_test.go b/internal/domain/redis_test.go new file mode 100644 index 0000000..3df7c50 --- /dev/null +++ b/internal/domain/redis_test.go @@ -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") +} diff --git a/internal/domain/repository/timeline.go b/internal/domain/repository/timeline.go new file mode 100644 index 0000000..6beb3af --- /dev/null +++ b/internal/domain/repository/timeline.go @@ -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 +} diff --git a/internal/repository/timeline_sort_by_timestamp.go b/internal/repository/timeline_sort_by_timestamp.go new file mode 100644 index 0000000..fdef560 --- /dev/null +++ b/internal/repository/timeline_sort_by_timestamp.go @@ -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 +} diff --git a/internal/repository/timeline_sort_by_timestamp_test.go b/internal/repository/timeline_sort_by_timestamp_test.go new file mode 100644 index 0000000..0ada538 --- /dev/null +++ b/internal/repository/timeline_sort_by_timestamp_test.go @@ -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) + } + }) + } +} diff --git a/internal/server/commentservice/comment_service_server.go b/internal/server/commentservice/comment_service_server.go index f434bad..9fed63f 100644 --- a/internal/server/commentservice/comment_service_server.go +++ b/internal/server/commentservice/comment_service_server.go @@ -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" ) diff --git a/internal/server/postservice/post_service_server.go b/internal/server/postservice/post_service_server.go index 07d52d3..639c1f4 100644 --- a/internal/server/postservice/post_service_server.go +++ b/internal/server/postservice/post_service_server.go @@ -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" ) diff --git a/internal/server/timelineservice/timeline_service_server.go b/internal/server/timelineservice/timeline_service_server.go index 7e641f4..186fef5 100644 --- a/internal/server/timelineservice/timeline_service_server.go +++ b/internal/server/timelineservice/timeline_service_server.go @@ -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" )