feat: add product tags index
This commit is contained in:
parent
f6e936a760
commit
6e47895174
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue