From 6e47895174862f15bc7efdbe03f09d43b7545932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Fri, 21 Mar 2025 07:31:35 +0800 Subject: [PATCH] feat: add product tags index --- pkg/repository/product_tags_test.go | 497 ++++++++++++++++++++++++++++ 1 file changed, 497 insertions(+) create mode 100644 pkg/repository/product_tags_test.go diff --git a/pkg/repository/product_tags_test.go b/pkg/repository/product_tags_test.go new file mode 100644 index 0000000..c4a7e42 --- /dev/null +++ b/pkg/repository/product_tags_test.go @@ -0,0 +1,497 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/product" + "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "errors" + "fmt" + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/core/stores/mon" + "github.com/zeromicro/go-zero/core/stores/redis" + "go.mongodb.org/mongo-driver/bson/primitive" + "testing" + "time" +) + +func SetupTestProductTagsRepo(db string) (repository.TagRepo, func(), error) { + h, p, tearDown, err := startMongoContainer() + if err != nil { + return nil, nil, err + } + s, _ := miniredis.Run() + + conf := &mgo.Conf{ + Schema: Schema, + Host: fmt.Sprintf("%s:%s", h, p), + Database: db, + MaxStaleness: 300, + MaxPoolSize: 100, + MinPoolSize: 100, + MaxConnIdleTime: 300, + Compressors: []string{}, + EnableStandardReadWriteSplitMode: false, + ConnectTimeoutMs: 3000, + } + + cacheConf := cache.CacheConf{ + cache.NodeConf{ + RedisConf: redis.RedisConf{ + Host: s.Addr(), + Type: redis.NodeType, + }, + Weight: 100, + }, + } + + cacheOpts := []cache.Option{ + cache.WithExpiry(1000 * time.Microsecond), + cache.WithNotFoundExpiry(1000 * time.Microsecond), + } + + param := TagsRepositoryParam{ + Conf: conf, + CacheConf: cacheConf, + CacheOpts: cacheOpts, + DBOpts: []mon.Option{ + mgo.SetCustomDecimalType(), + mgo.InitMongoOptions(*conf), + }, + } + + repo := NewTagsRepository(param) + _, _ = repo.IndexTags20250317001UP(context.Background()) + _, _ = repo.IndexTagsBinding20250317001UP(context.Background()) + + return repo, tearDown, nil +} + +func TestCreateTags(t *testing.T) { + repo, tearDown, err := SetupTestProductTagsRepo("testDB") + require.NoError(t, err) + defer tearDown() + + ctx := context.Background() + + tests := []struct { + name string + inputTag *entity.Tags + check func(t *testing.T, tag *entity.Tags) + expectErr bool + }{ + { + name: "Insert tag with zero ID", + inputTag: &entity.Tags{ + // ID 為零值,Create 會自動生成 + Types: product.ItemTypeSkill, + Name: "Tag A", + ShowType: product.ShowTypeNormal, + Cover: nil, + }, + check: func(t *testing.T, tag *entity.Tags) { + require.False(t, tag.ID.IsZero(), "ID should be generated") + assert.NotZero(t, tag.CreatedAt, "CreatedAt should be set") + assert.NotZero(t, tag.UpdatedAt, "UpdatedAt should be set") + assert.Equal(t, "Tag A", tag.Name) + }, + expectErr: false, + }, + { + name: "Insert tag with preset ID", + inputTag: &entity.Tags{ + ID: primitive.NewObjectID(), // 預先設定 ID,不會被覆蓋 + Types: product.ItemTypeSkill, + Name: "Tag B", + ShowType: product.ShowTypeNormal, + Cover: nil, + // 預設時間欄位 + CreatedAt: 123456789, + UpdatedAt: 123456789, + }, + check: func(t *testing.T, tag *entity.Tags) { + assert.False(t, tag.ID.IsZero()) + assert.Equal(t, "Tag B", tag.Name) + // 保持原有的 CreatedAt 與 UpdatedAt 值 + assert.Equal(t, int64(123456789), tag.CreatedAt) + assert.Equal(t, int64(123456789), tag.UpdatedAt) + }, + expectErr: false, + }, + } + + for _, tc := range tests { + tc := tc // capture range variable + t.Run(tc.name, func(t *testing.T) { + err := repo.Create(ctx, tc.inputTag) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + // 驗證 Create 方法對輸入資料的影響 + tc.check(t, tc.inputTag) + + // 可選:利用 GetByID 再從資料庫中讀取進一步驗證 + tagFromDB, err := repo.GetByID(ctx, tc.inputTag.ID.Hex()) + require.NoError(t, err) + assert.Equal(t, tc.inputTag.Name, tagFromDB.Name) + assert.Equal(t, tc.inputTag.Types, tagFromDB.Types) + assert.Equal(t, tc.inputTag.ShowType, tagFromDB.ShowType) + } + }) + } +} + +func TestGetTagByID(t *testing.T) { + repo, tearDown, err := SetupTestProductTagsRepo("testDB") + require.NoError(t, err) + defer tearDown() + + ctx := context.Background() + + // 先建立一筆測試資料 + tag := &entity.Tags{ + Types: product.ItemTypeSkill, + Name: "Sample Tag", + ShowType: product.ShowTypeNormal, + Cover: nil, + } + err = repo.Create(ctx, tag) + require.NoError(t, err) + + tests := []struct { + name string + inputID string + expectedName string + expectedErr error + }{ + { + name: "Valid tag ID", + inputID: tag.ID.Hex(), + expectedName: "Sample Tag", + expectedErr: nil, + }, + { + name: "Invalid ObjectID format", + inputID: "invalid-hex", + expectedName: "", + // 這裡預期錯誤內容會包含 "hex" 字樣,故使用 nil 來做後續判斷 + expectedErr: errors.New("invalid"), + }, + { + name: "Tag not found", + inputID: primitive.NewObjectID().Hex(), + expectedName: "", + expectedErr: ErrNotFound, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + res, err := repo.GetByID(ctx, tt.inputID) + if tt.expectedErr != nil { + require.Error(t, err) + if tt.name == "Invalid ObjectID format" { + assert.Contains(t, err.Error(), "hex") + } else { + assert.Equal(t, tt.expectedErr, err) + } + assert.Nil(t, res) + } else { + require.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, tt.expectedName, res.Name) + } + }) + } +} + +func TestUpdateTag(t *testing.T) { + repo, tearDown, err := SetupTestProductTagsRepo("testDB") + require.NoError(t, err) + defer tearDown() + + ctx := context.Background() + + // 先建立一筆初始的 Tag 測試資料 + origTag := &entity.Tags{ + Types: product.ItemTypeSkill, + ShowType: product.ShowTypeNormal, + Name: "Original Name", + } + err = repo.Create(ctx, origTag) + require.NoError(t, err) + + tests := []struct { + name string + id string + params repository.TagModifyParams + expectedName string + expectedTypes product.ItemType + expectedShowType product.ShowType + expectedCover *string + expectErr bool + }{ + { + name: "Update all fields", + id: origTag.ID.Hex(), + params: repository.TagModifyParams{ + Name: ptr("New Name"), + Types: ptr(product.ItemTypeSkill), + ShowType: ptr(product.ShowTypeNormal), + Cover: ptr("new-cover.jpg"), + }, + expectedName: "New Name", + expectedTypes: product.ItemTypeSkill, + expectedShowType: product.ShowTypeNormal, + expectedCover: ptr("new-cover.jpg"), + expectErr: false, + }, + { + name: "Update only name", + id: origTag.ID.Hex(), + params: repository.TagModifyParams{ + Name: ptr("Another Name"), + }, + // 其他欄位保持原值 + expectedName: "Another Name", + expectedTypes: origTag.Types, + expectedShowType: origTag.ShowType, + expectedCover: ptr("new-cover.jpg"), + expectErr: false, + }, + { + name: "Invalid ObjectID", + id: "invalid-id", + params: repository.TagModifyParams{ + Name: ptr("Should Not Update"), + }, + expectErr: true, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + err := repo.Update(ctx, tt.id, tt.params) + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + // 讀取更新後的資料 + updated, err := repo.GetByID(ctx, tt.id) + require.NoError(t, err) + assert.Equal(t, tt.expectedName, updated.Name) + assert.Equal(t, tt.expectedTypes, updated.Types) + assert.Equal(t, tt.expectedShowType, updated.ShowType) + if tt.expectedCover == nil { + assert.Nil(t, updated.Cover) + } else { + assert.Equal(t, *tt.expectedCover, *updated.Cover) + } + // 驗證 updated_at 一定被更新(大於 0) + assert.NotZero(t, updated.UpdatedAt) + } + }) + } +} + +func TestDeleteTag(t *testing.T) { + repo, tearDown, err := SetupTestProductTagsRepo("testDB") + require.NoError(t, err) + defer tearDown() + + ctx := context.Background() + + // 插入一筆測試資料 + tag := &entity.Tags{ + Types: product.ItemTypeSkill, + ShowType: product.ShowTypeNormal, + Name: "Test Tag", + // Cover 為 nil + } + err = repo.Create(ctx, tag) + require.NoError(t, err) + + tests := []struct { + name string + id string + expectErr error // 預期的錯誤,若為 nil 表示預期成功 + checkDelete bool // 若為 true,表示刪除後需驗證資料不存在 + }{ + { + name: "Delete existing tag", + id: tag.ID.Hex(), + expectErr: nil, + checkDelete: true, + }, + { + name: "Invalid ObjectID format", + id: "invalid-id", + expectErr: ErrInvalidObjectID, + checkDelete: false, + }, + { + name: "Delete non-existent tag", + id: primitive.NewObjectID().Hex(), + expectErr: nil, + checkDelete: false, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + err := repo.Delete(ctx, tt.id) + if tt.expectErr != nil { + require.Error(t, err) + assert.Equal(t, tt.expectErr, err) + } else { + require.NoError(t, err) + if tt.checkDelete { + // 刪除成功後,透過 GetByID 應查無資料 + _, err := repo.GetByID(ctx, tt.id) + require.Error(t, err) + assert.Equal(t, ErrNotFound, err) + } + } + }) + } +} + +func TestListTags(t *testing.T) { + repo, tearDown, err := SetupTestProductTagsRepo("testDB") + require.NoError(t, err) + defer tearDown() + + ctx := context.Background() + now := time.Now().UnixNano() + + // 建立測試資料,手動指定 ID 與時間(不讓 Create 自動覆蓋) + tag1 := &entity.Tags{ + ID: primitive.NewObjectID(), + Name: "Tag A", + Types: product.ItemTypeSkill, + ShowType: product.ShowTypeNormal, + CreatedAt: now + 300, + UpdatedAt: now + 300, + } + tag2 := &entity.Tags{ + ID: primitive.NewObjectID(), + Name: "Tag B", + Types: product.ItemTypeProduct, + ShowType: product.ShowTypeNormal, + CreatedAt: now + 200, + UpdatedAt: now + 200, + } + tag3 := &entity.Tags{ + ID: primitive.NewObjectID(), + Types: product.ItemTypeProduct, + Name: "Tag C", + ShowType: product.ShowTypeNormal, + CreatedAt: now + 100, + UpdatedAt: now + 100, + } + + // 插入測試資料 + err = repo.Create(ctx, tag1) + require.NoError(t, err) + err = repo.Create(ctx, tag2) + require.NoError(t, err) + err = repo.Create(ctx, tag3) + require.NoError(t, err) + + tests := []struct { + name string + params repository.TagQueryParams + expectCount int64 + expectIDsOrder []primitive.ObjectID + }{ + { + name: "Filter by Name", + params: repository.TagQueryParams{ + Name: ptr("Tag A"), + PageSize: 10, + PageIndex: 1, + }, + expectCount: 1, + expectIDsOrder: []primitive.ObjectID{tag1.ID}, + }, + { + name: "Filter by Types = type1", + params: repository.TagQueryParams{ + Types: ptr(product.ItemTypeProduct), + PageSize: 10, + PageIndex: 1, + }, + // tag1與 tag2 符合條件 + expectCount: 2, + // 排序依 updated_at 由大到小,故 tag1 (now+300) 在前,接著 tag2 (now+200) + expectIDsOrder: []primitive.ObjectID{tag1.ID, tag2.ID}, + }, + { + name: "Filter by ShowType = show1", + params: repository.TagQueryParams{ + ShowType: ptr(product.ShowTypeNormal), + PageSize: 10, + PageIndex: 1, + }, + // tag1與 tag3 符合條件 + expectCount: 3, + // 排序:tag1 (now+300) 在前,tag3 (now+100) 在後 + expectIDsOrder: []primitive.ObjectID{tag1.ID, tag2.ID, tag3.ID}, + }, + { + name: "Filter by Types=type1 and ShowType=show1", + params: repository.TagQueryParams{ + Types: ptr(product.ItemTypeProduct), + ShowType: ptr(product.ShowTypeNormal), + PageSize: 10, + PageIndex: 1, + }, + // 只有 tag1 同時符合兩個條件 + expectCount: 2, + expectIDsOrder: []primitive.ObjectID{tag1.ID, tag3.ID}, + }, + { + name: "Pagination works: PageIndex 1", + params: repository.TagQueryParams{ + PageSize: 2, + PageIndex: 1, + }, + // 全部 3 筆資料符合查詢條件,但分頁僅返回前 2 筆,排序依 updated_at 由大到小:tag1, tag2, tag3 + expectCount: 3, + expectIDsOrder: []primitive.ObjectID{tag1.ID, tag2.ID}, + }, + { + name: "Pagination works: PageIndex 2", + params: repository.TagQueryParams{ + PageSize: 2, + PageIndex: 2, + }, + // 第二頁應返回剩下的 1 筆:tag3 + expectCount: 3, + expectIDsOrder: []primitive.ObjectID{tag3.ID}, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + result, count, err := repo.List(ctx, tt.params) + require.NoError(t, err) + assert.Equal(t, tt.expectCount, count) + + var gotIDs []primitive.ObjectID + for _, tag := range result { + gotIDs = append(gotIDs, tag.ID) + } + assert.Equal(t, tt.expectIDsOrder, gotIDs) + }) + } +}