app-cloudep-product-service/pkg/repository/product_item_basic_test.go

801 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"context"
"fmt"
"testing"
"time"
"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"
"github.com/alicebob/miniredis/v2"
"github.com/shopspring/decimal"
"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"
"google.golang.org/protobuf/proto"
)
func SetupTestProductItemRepository(db string) (repository.ProductItemRepository, 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 := ProductItemRepositoryParam{
Conf: conf,
CacheConf: cacheConf,
CacheOpts: cacheOpts,
DBOpts: []mon.Option{
mgo.SetCustomDecimalType(),
mgo.InitMongoOptions(*conf),
},
}
repo := NewProductItemRepository(param)
_, _ = repo.Index20250317001UP(context.Background())
return repo, tearDown, nil
}
func TestInsertProductItems(t *testing.T) {
model, tearDown, err := SetupTestProductItemRepository("testDB")
defer tearDown()
assert.NoError(t, err)
// 建立多筆 items
items := []entity.ProductItems{
{
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Item A",
},
{
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Item B",
},
}
// 呼叫插入
err = model.Insert(context.Background(), items)
assert.NoError(t, err)
// 驗證插入是否成功(逐筆查詢)
for _, item := range items {
// 檢查 ID 是否自動填入
assert.False(t, item.ID.IsZero(), "ID should be generated")
assert.NotZero(t, item.CreatedAt)
assert.NotZero(t, item.UpdatedAt)
// 查詢 DB 確認存在
var result *entity.ProductItems
result, err = model.FindByID(context.Background(), item.ID.Hex())
assert.NoError(t, err)
assert.Equal(t, item.Name, result.Name)
}
// 🧪 空陣列插入測試(不應報錯)
t.Run("Insert empty slice", func(t *testing.T) {
err := model.Insert(context.Background(), []entity.ProductItems{})
assert.NoError(t, err)
})
}
func TestDeleteProductItem(t *testing.T) {
model, tearDown, err := SetupTestProductItemRepository("testDB")
defer tearDown()
assert.NoError(t, err)
// 建立多筆 items
items := []entity.ProductItems{
{
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Item A",
},
{
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Item B",
},
}
// 呼叫插入
err = model.Insert(context.Background(), items)
assert.NoError(t, err)
tests := []struct {
name string
inputID string
expectErr error
checkAfter bool // 是否需要確認資料已刪除
}{
{
name: "Delete existing product",
inputID: items[0].ID.Hex(),
expectErr: nil,
checkAfter: true,
},
{
name: "Delete non-existing product",
inputID: primitive.NewObjectID().Hex(),
expectErr: nil,
checkAfter: false,
},
{
name: "Invalid ObjectID format",
inputID: "not-an-object-id",
expectErr: ErrInvalidObjectID,
checkAfter: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := model.Delete(context.Background(), []string{tt.inputID})
if tt.expectErr != nil {
assert.ErrorIs(t, err, tt.expectErr)
} else {
assert.NoError(t, err)
}
// 驗證資料是否真的刪除了(僅當需要)
if tt.checkAfter {
_, err := model.FindByID(context.Background(), tt.inputID)
assert.ErrorIs(t, err, ErrNotFound)
}
})
}
}
func TestListProductItem(t *testing.T) {
model, tearDown, err := SetupTestProductItemRepository("testDB")
defer tearDown()
assert.NoError(t, err)
now := time.Now().UnixNano()
rfcID := primitive.NewObjectID().Hex()
// 插入測試資料
item1 := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: rfcID,
Name: "Item A",
IsFree: true,
Status: product.StatusActive,
CreatedAt: now,
UpdatedAt: now,
}
item2 := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: rfcID,
Name: "Item B",
IsFree: false,
Status: product.StatusInactive,
CreatedAt: now,
UpdatedAt: now,
}
item3 := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: rfcID,
Name: "Item C",
IsFree: true,
Status: product.StatusActive,
CreatedAt: now,
UpdatedAt: now,
}
err = model.Insert(context.Background(), []entity.ProductItems{item1, item2, item3})
assert.NoError(t, err)
tests := []struct {
name string
params repository.ProductItemQueryParams
expectCount int64
expectIDs []primitive.ObjectID
}{
{
name: "Filter by ReferenceID",
params: repository.ProductItemQueryParams{
ReferenceID: ptr(rfcID),
PageSize: 10,
PageIndex: 1,
},
expectCount: 3,
expectIDs: []primitive.ObjectID{item3.ID, item2.ID, item1.ID},
},
{
name: "Filter by IsFree = true",
params: repository.ProductItemQueryParams{
IsFree: ptr(true),
PageSize: 10,
PageIndex: 1,
},
expectCount: 2,
expectIDs: []primitive.ObjectID{item3.ID, item1.ID},
},
{
name: "Filter by Status = 2",
params: repository.ProductItemQueryParams{
Status: ptr(product.StatusInactive),
PageSize: 10,
PageIndex: 1,
},
expectCount: 1,
expectIDs: []primitive.ObjectID{item2.ID},
},
{
name: "Filter by ItemIDs",
params: repository.ProductItemQueryParams{
ItemID: []string{item1.ID.Hex(), item2.ID.Hex()},
PageSize: 10,
PageIndex: 1,
},
expectCount: 2,
expectIDs: []primitive.ObjectID{item2.ID, item1.ID},
},
{
name: "Pagination works",
params: repository.ProductItemQueryParams{
PageSize: 1,
PageIndex: 2,
},
expectCount: 3,
expectIDs: []primitive.ObjectID{item2.ID},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results, count, err := model.ListProductItem(context.Background(), tt.params)
assert.NoError(t, err)
assert.Equal(t, tt.expectCount, count)
var gotIDs []primitive.ObjectID
for _, r := range results {
gotIDs = append(gotIDs, r.ID)
}
assert.ElementsMatch(t, tt.expectIDs, gotIDs)
})
}
}
func TestDeleteByReferenceID(t *testing.T) {
model, tearDown, err := SetupTestProductItemRepository("testDB")
defer tearDown()
assert.NoError(t, err)
ctx := context.Background()
now := time.Now().UnixNano()
// 插入測試資料
refID := primitive.NewObjectID().Hex()
item1 := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: refID,
Name: "Item A",
CreatedAt: now,
UpdatedAt: now,
}
item2 := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: refID,
Name: "Item B",
CreatedAt: now,
UpdatedAt: now,
}
itemOther := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Should not be deleted",
CreatedAt: now,
UpdatedAt: now,
}
err = model.Insert(ctx, []entity.ProductItems{item1, item2, itemOther})
assert.NoError(t, err)
tests := []struct {
name string
referenceID string
expectDeleted []primitive.ObjectID
expectRemained []primitive.ObjectID
}{
{
name: "Delete existing reference_id items",
referenceID: refID,
expectDeleted: []primitive.ObjectID{item1.ID, item2.ID},
expectRemained: []primitive.ObjectID{itemOther.ID},
},
{
name: "Delete non-existent reference_id",
referenceID: "no-match-ref",
expectDeleted: nil,
expectRemained: []primitive.ObjectID{itemOther.ID},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := model.DeleteByReferenceID(ctx, tt.referenceID)
assert.NoError(t, err)
// 檢查指定應被刪除的項目是否真的被刪除
for _, id := range tt.expectDeleted {
_, err = model.FindByID(ctx, id.Hex())
assert.Error(t, err, "Expected item to be deleted: "+id.Hex())
}
// 檢查不應刪除的項目仍存在
for _, id := range tt.expectRemained {
_, err = model.FindByID(ctx, id.Hex())
assert.NoError(t, err, "Expected item to remain: "+id.Hex())
}
})
}
}
func TestUpdateProductItem(t *testing.T) {
model, tearDown, err := SetupTestProductItemRepository("testDB")
defer tearDown()
assert.NoError(t, err)
ctx := context.Background()
// 建立一筆測試資料
item := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Original Name",
Stock: 10,
Price: decimal.NewFromInt(500),
IsUnLimit: false,
IsFree: false,
CreatedAt: time.Now().UnixNano(),
UpdatedAt: time.Now().UnixNano(),
}
err = model.Insert(ctx, []entity.ProductItems{item})
assert.NoError(t, err)
tests := []struct {
name string
id string
update *repository.ProductUpdateItem
expectErr error
validate func(t *testing.T, updated *entity.ProductItems)
}{
{
name: "Update is_un_limit and is_free",
id: item.ID.Hex(),
update: &repository.ProductUpdateItem{
IsUnLimit: ptr(true),
IsFree: ptr(true),
},
expectErr: nil,
validate: func(t *testing.T, updated *entity.ProductItems) {
assert.True(t, updated.IsUnLimit)
assert.True(t, updated.IsFree)
},
},
{
name: "Update SKU and TimeSeries",
id: item.ID.Hex(),
update: &repository.ProductUpdateItem{
SKU: ptr("SKU-XYZ-001"),
TimeSeries: ptr(product.TimeSeriesTenMinutes),
},
expectErr: nil,
validate: func(t *testing.T, updated *entity.ProductItems) {
assert.Equal(t, "SKU-XYZ-001", updated.SKU)
assert.Equal(t, product.TimeSeriesTenMinutes, updated.TimeSeries)
},
},
{
name: "Update Media field",
id: item.ID.Hex(),
update: &repository.ProductUpdateItem{
Media: []entity.Media{
{Sort: 1, Type: "image", URL: "https://example.com/img.jpg"},
},
},
expectErr: nil,
validate: func(t *testing.T, updated *entity.ProductItems) {
assert.Len(t, updated.Media, 1)
assert.Equal(t, "image", updated.Media[0].Type)
},
},
{
name: "Update CustomFields",
id: item.ID.Hex(),
update: &repository.ProductUpdateItem{
CustomFields: []entity.CustomFields{
{Key: "color", Value: "red"},
},
},
expectErr: nil,
validate: func(t *testing.T, updated *entity.ProductItems) {
assert.Len(t, updated.CustomFields, 1)
assert.Equal(t, "color", updated.CustomFields[0].Key)
},
},
{
name: "Update Freight",
id: item.ID.Hex(),
update: &repository.ProductUpdateItem{
Freight: []entity.CustomFields{
{
Key: "color",
Value: "red",
},
},
},
expectErr: nil,
validate: func(t *testing.T, updated *entity.ProductItems) {
assert.Equal(t, "color", updated.Freight[0].Key)
assert.Equal(t, "red", updated.Freight[0].Value)
},
},
{
name: "Update name field",
id: item.ID.Hex(),
update: &repository.ProductUpdateItem{
Name: ptr("Updated Name"),
},
expectErr: nil,
validate: func(t *testing.T, updated *entity.ProductItems) {
assert.Equal(t, "Updated Name", updated.Name)
},
},
{
name: "Update stock and price",
id: item.ID.Hex(),
update: &repository.ProductUpdateItem{
Stock: proto.Int64(99),
Price: ptr(decimal.NewFromInt(999)),
},
expectErr: nil,
validate: func(t *testing.T, updated *entity.ProductItems) {
assert.Equal(t, uint64(99), updated.Stock)
assert.Equal(t, "999", updated.Price.String())
},
},
{
name: "Invalid ObjectID",
id: "not-an-id",
update: &repository.ProductUpdateItem{Name: ptr("Invalid")},
expectErr: ErrInvalidObjectID,
},
{
name: "Empty update struct",
id: item.ID.Hex(),
update: &repository.ProductUpdateItem{}, // no fields
expectErr: fmt.Errorf("no fields to update"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := model.Update(ctx, tt.id, tt.update)
if tt.expectErr != nil {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectErr.Error())
return
}
assert.NoError(t, err)
updated, err := model.FindByID(ctx, item.ID.Hex())
assert.NoError(t, err)
if tt.validate != nil {
tt.validate(t, updated)
}
})
}
}
func TestUpdateStatus_TableDriven(t *testing.T) {
repo, tearDown, err := SetupTestProductItemRepository("testDB")
require.NoError(t, err)
defer tearDown()
ctx := context.Background()
// Insert a sample product item.
now := time.Now().UnixNano()
item := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Test Item",
Status: product.StatusInactive, // initial status
CreatedAt: now,
UpdatedAt: now,
}
err = repo.Insert(ctx, []entity.ProductItems{item})
require.NoError(t, err)
tests := []struct {
name string
id string
newStatus product.ItemStatus
expectErr error
check bool // whether to verify the update in DB
}{
{
name: "Valid update",
id: item.ID.Hex(),
newStatus: product.StatusActive,
expectErr: nil,
check: true,
},
{
name: "Invalid ObjectID",
id: "invalid-id",
newStatus: product.StatusActive,
expectErr: ErrInvalidObjectID,
check: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := repo.UpdateStatus(ctx, tt.id, tt.newStatus)
if tt.expectErr != nil {
assert.ErrorIs(t, err, tt.expectErr)
} else {
assert.NoError(t, err)
}
// If expected to check update, verify that the product's status is updated.
if tt.check {
updated, err := repo.FindByID(ctx, tt.id)
assert.NoError(t, err)
assert.Equal(t, tt.newStatus, updated.Status)
}
})
}
}
func TestIncSalesCount(t *testing.T) {
repo, tearDown, err := SetupTestProductItemRepository("testDB")
require.NoError(t, err)
defer tearDown()
ctx := context.Background()
// Insert a sample product item with initial SalesCount = 0.
now := time.Now().UnixNano()
item := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Sales Count Test Item",
SalesCount: 0,
CreatedAt: now,
UpdatedAt: now,
}
err = repo.Insert(ctx, []entity.ProductItems{item})
require.NoError(t, err)
tests := []struct {
name string
id string
count int64
expectErr error
check bool // whether to verify the updated sales count in the DB.
}{
{
name: "Increment sales count by 5",
id: item.ID.Hex(),
count: 5,
expectErr: nil,
check: true,
},
{
name: "Invalid ObjectID",
id: "invalid-id",
count: 3,
expectErr: ErrInvalidObjectID,
check: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := repo.IncSalesCount(ctx, tt.id, tt.count)
if tt.expectErr != nil {
assert.ErrorIs(t, err, tt.expectErr)
} else {
assert.NoError(t, err)
}
// If check is true, verify that the sales_count is updated correctly.
if tt.check {
updated, err := repo.FindByID(ctx, tt.id)
assert.NoError(t, err)
// Since initial SalesCount was 0, after increment it should equal tt.count.
assert.Equal(t, uint64(tt.count), updated.SalesCount)
}
})
}
}
func TestDecSalesCount(t *testing.T) {
repo, tearDown, err := SetupTestProductItemRepository("testDB")
require.NoError(t, err)
defer tearDown()
ctx := context.Background()
now := time.Now().UnixNano()
// Insert an item with an initial SalesCount of 10.
item := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Dec Sales Count Test Item",
SalesCount: 10,
CreatedAt: now,
UpdatedAt: now,
}
// Insert an item with SalesCount equal to 0 (to test underflow behavior).
zeroItem := entity.ProductItems{
ID: primitive.NewObjectID(),
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Zero Sales Count Item",
SalesCount: 0,
CreatedAt: now,
UpdatedAt: now,
}
err = repo.Insert(ctx, []entity.ProductItems{item, zeroItem})
require.NoError(t, err)
tests := []struct {
name string
id string
decCount int64
expectErr error
check bool // whether to verify the updated sales count in the DB
expected uint64 // expected SalesCount if check is true
}{
{
name: "Valid decrement from 10",
id: item.ID.Hex(),
decCount: 3,
expectErr: nil,
check: true,
expected: 7,
},
{
name: "Invalid ObjectID",
id: "invalid-id",
decCount: 3,
expectErr: ErrInvalidObjectID,
check: false,
},
{
name: "Decrement from zero (should not allow underflow)",
id: zeroItem.ID.Hex(),
decCount: 5,
expectErr: nil,
check: true,
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := repo.DecSalesCount(ctx, tt.id, tt.decCount)
if tt.expectErr != nil {
assert.Error(t, err)
// Check that the error message contains "failed to decrease stock" (for the underflow case)
if tt.expectErr.Error() != ErrInvalidObjectID.Error() {
assert.Contains(t, err.Error(), "failed to decrease stock")
} else {
assert.ErrorIs(t, err, tt.expectErr)
}
} else {
assert.NoError(t, err)
}
// If expected to check, verify the updated SalesCount.
if tt.check {
updated, err := repo.FindByID(ctx, tt.id)
assert.NoError(t, err)
assert.Equal(t, tt.expected, updated.SalesCount)
}
})
}
}
func TestGetSalesCount(t *testing.T) {
// 取得測試 repository
repo, tearDown, err := SetupTestProductItemRepository("testDB")
require.NoError(t, err)
defer tearDown()
ctx := context.Background()
// 預先建立測試資料,設定各項目的 SalesCount 值
testItems := []entity.ProductItems{
{
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Test Item 1",
SalesCount: 5,
},
{
ReferenceID: primitive.NewObjectID().Hex(),
Name: "Test Item 2",
SalesCount: 15,
},
}
// 插入測試資料
err = repo.Insert(ctx, testItems)
require.NoError(t, err)
// 建立一組包含有效與無效 ID 的字串陣列
validIDs := []string{
testItems[0].ID.Hex(),
testItems[1].ID.Hex(),
}
// 在陣列前面加上一個無法轉換的 ID
mixedIDs := append([]string{"invalidID"}, validIDs...)
t.Run("with valid and invalid IDs", func(t *testing.T) {
salesCounts, err := repo.GetSalesCount(ctx, mixedIDs)
require.NoError(t, err)
// 預期只會回傳有效的兩筆資料
require.Len(t, salesCounts, len(validIDs))
// 驗證每筆資料的 SalesCount 是否正確
for _, sc := range salesCounts {
switch sc.ID {
case testItems[0].ID.Hex():
assert.Equal(t, uint64(5), sc.Count)
case testItems[1].ID.Hex():
assert.Equal(t, uint64(15), sc.Count)
default:
t.Errorf("unexpected product item ID: %s", sc.ID)
}
}
})
t.Run("with all invalid IDs", func(t *testing.T) {
salesCounts, err := repo.GetSalesCount(ctx, []string{"badid1", "badid2"})
// 因無法轉換成 ObjectID預期會回傳 ErrInvalidObjectID
assert.Error(t, err)
assert.Equal(t, ErrInvalidObjectID, err)
assert.Nil(t, salesCounts)
})
}