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

501 lines
13 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"testing"
"time"
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity"
"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/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/redis"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
func SetupTestProductRepository(db string) (repository.ProductRepository, 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 := ProductRepositoryParam{
Conf: conf,
CacheConf: cacheConf,
CacheOpts: cacheOpts,
}
repo := NewProductRepository(param)
_, _ = repo.Index20250317001UP(context.Background())
return repo, tearDown, nil
}
func TestListProduct(t *testing.T) {
model, tearDown, err := SetupTestProductRepository("testDB")
defer tearDown()
fmt.Println("ddddddddddddddddddddd", err.Error())
assert.NoError(t, err)
now := time.Now()
products := []*entity.Product{
{
UID: "user1",
Title: "Product 1",
IsPublished: true,
Amount: 100,
StartTime: ptr(now.Add(-24 * time.Hour).Unix()),
EndTime: ptr(now.Add(24 * time.Hour).Unix()),
Category: "tech",
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
Slug: ptr("product-1"),
Details: ptr("details..."),
},
{
UID: "user2",
Title: "Product 2",
IsPublished: false,
Amount: 200,
StartTime: ptr(now.Add(-48 * time.Hour).Unix()),
EndTime: ptr(now.Add(48 * time.Hour).Unix()),
Category: "health",
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
Slug: ptr("product-2"),
Details: ptr("details..."),
},
{
UID: "user1",
Title: "Product 3",
IsPublished: true,
Amount: 300,
StartTime: ptr(now.Add(-72 * time.Hour).Unix()),
EndTime: ptr(now.Add(72 * time.Hour).Unix()),
Category: "tech",
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
Slug: ptr("product-3"),
Details: ptr("details..."),
},
}
for _, p := range products {
err = model.Insert(context.Background(), p)
assert.NoError(t, err)
}
tests := []struct {
name string
params *repository.ProductQueryParams
expectCount int64
expectIDs []string
}{
{
name: "Filter by UID",
params: &repository.ProductQueryParams{
UID: ptr("user1"),
PageSize: 10,
PageIndex: 1,
},
expectCount: 2,
expectIDs: []string{products[2].ID.Hex(), products[0].ID.Hex()},
},
{
name: "Filter by Published",
params: &repository.ProductQueryParams{
IsPublished: ptr(true),
PageSize: 10,
PageIndex: 1,
},
expectCount: 2,
expectIDs: []string{products[2].ID.Hex(), products[0].ID.Hex()},
},
{
name: "Filter by Category",
params: &repository.ProductQueryParams{
Category: ptr("tech"),
PageSize: 10,
PageIndex: 1,
},
expectCount: 2,
expectIDs: []string{products[2].ID.Hex(), products[0].ID.Hex()},
},
{
name: "Filter by StartTime >= now - 36h (should get P1 & P2)",
params: &repository.ProductQueryParams{
StartTime: ptr(now.Add(-36 * time.Hour).Unix()),
PageSize: 10,
PageIndex: 1,
},
expectCount: 1,
expectIDs: []string{products[0].ID.Hex()},
},
{
name: "Filter by EndTime <= now + 30h (should get P1)",
params: &repository.ProductQueryParams{
EndTime: ptr(now.Add(30 * time.Hour).Unix()),
PageSize: 10,
PageIndex: 1,
},
expectCount: 1,
expectIDs: []string{products[0].ID.Hex()},
},
{
name: "Filter by Slug = product-2",
params: &repository.ProductQueryParams{
Slug: ptr("product-2"),
PageSize: 10,
PageIndex: 1,
},
expectCount: 1,
expectIDs: []string{products[1].ID.Hex()},
},
{
name: "Filter by UID + Category + Published",
params: &repository.ProductQueryParams{
UID: ptr("user1"),
Category: ptr("tech"),
IsPublished: ptr(true),
PageSize: 10,
PageIndex: 1,
},
expectCount: 2,
expectIDs: []string{products[2].ID.Hex(), products[0].ID.Hex()},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, count, err := model.ListProduct(context.Background(), tt.params)
assert.NoError(t, err)
assert.Equal(t, tt.expectCount, count)
var resultIDs []string
for _, r := range result {
resultIDs = append(resultIDs, r.ID.Hex())
}
assert.Equal(t, tt.expectIDs, resultIDs)
})
}
}
func TestFindOneBySlug(t *testing.T) {
model, tearDown, err := SetupTestProductRepository("testDB")
defer tearDown()
assert.NoError(t, err)
now := time.Now()
products := []*entity.Product{
{
UID: "user1",
Title: "Product 1",
IsPublished: true,
Slug: ptr("product-1"),
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
},
{
UID: "user2",
Title: "Product 2",
IsPublished: true,
Slug: ptr("product-2"),
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
},
}
// 插入測試資料
for _, p := range products {
err = model.Insert(context.Background(), p)
assert.NoError(t, err)
}
tests := []struct {
name string
slug string
expectFound bool
expectID string
}{
{
name: "Found: product-1",
slug: "product-1",
expectFound: true,
expectID: products[0].ID.Hex(),
},
{
name: "Not Found: unknown-slug",
slug: "unknown-slug",
expectFound: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, err := model.FindOneBySlug(context.Background(), tt.slug)
if tt.expectFound {
assert.NoError(t, err)
assert.NotNil(t, res)
assert.Equal(t, tt.expectID, res.ID.Hex())
} else {
assert.Nil(t, res)
assert.ErrorIs(t, err, ErrNotFound)
}
})
}
}
func TestDeleteProduct(t *testing.T) {
model, tearDown, err := SetupTestProductRepository("testDB")
defer tearDown()
assert.NoError(t, err)
// 建立測試資料
now := time.Now()
product := &entity.Product{
UID: "user1",
Title: "Deletable Product",
IsPublished: true,
Slug: ptr("delete-slug"),
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
}
err = model.Insert(context.Background(), product)
assert.NoError(t, err)
t.Run("Delete existing product", func(t *testing.T) {
err := model.Delete(context.Background(), product.ID.Hex())
assert.NoError(t, err)
// 再查詢應該會是找不到
_, err = model.FindOneByID(context.Background(), product.ID.Hex())
assert.ErrorIs(t, err, ErrNotFound)
})
t.Run("Delete non-existing product", func(t *testing.T) {
invalidID := primitive.NewObjectID().Hex()
err := model.Delete(context.Background(), invalidID)
assert.ErrorIs(t, err, ErrNotFound)
})
}
func TestUpdateProduct(t *testing.T) {
model, tearDown, err := SetupTestProductRepository("testDB")
defer tearDown()
assert.NoError(t, err)
now := time.Now()
product := &entity.Product{
UID: "user1",
Title: "Original Title",
IsPublished: false,
Slug: ptr("original-slug"),
CreatedAt: now.UnixNano(),
UpdatedAt: now.UnixNano(),
}
err = model.Insert(context.Background(), product)
assert.NoError(t, err)
tests := []struct {
name string
update *repository.ProductUpdateParams
validate func(t *testing.T, updated *entity.Product)
}{
{
name: "Update title",
update: &repository.ProductUpdateParams{
Title: ptr("New Title"),
},
validate: func(t *testing.T, updated *entity.Product) {
assert.Equal(t, "New Title", updated.Title)
},
},
{
name: "Update is_published",
update: &repository.ProductUpdateParams{
IsPublished: ptr(true),
},
validate: func(t *testing.T, updated *entity.Product) {
assert.True(t, updated.IsPublished)
},
},
{
name: "Update start and end time",
update: &repository.ProductUpdateParams{
StartTime: ptr(now.Add(1 * time.Hour).UnixNano()),
EndTime: ptr(now.Add(2 * time.Hour).UnixNano()),
},
validate: func(t *testing.T, updated *entity.Product) {
assert.Equal(t, now.Add(1*time.Hour).UnixNano(), *updated.StartTime)
assert.Equal(t, now.Add(2*time.Hour).UnixNano(), *updated.EndTime)
},
},
{
name: "Update short description",
update: &repository.ProductUpdateParams{
ShortDescription: "Short desc here",
},
validate: func(t *testing.T, updated *entity.Product) {
assert.Equal(t, "Short desc here", updated.ShortDescription)
},
},
{
name: "Update details, short_title, slug",
update: &repository.ProductUpdateParams{
Details: ptr("Updated details"),
ShortTitle: ptr("ShortT"),
Slug: ptr("new-slug"),
},
validate: func(t *testing.T, updated *entity.Product) {
assert.Equal(t, "Updated details", *updated.Details)
assert.Equal(t, "ShortT", *updated.ShortTitle)
assert.Equal(t, "new-slug", *updated.Slug)
},
},
{
name: "Update amount",
update: &repository.ProductUpdateParams{
Amount: ptr(uint64(500)),
},
validate: func(t *testing.T, updated *entity.Product) {
assert.Equal(t, uint64(500), updated.Amount)
},
},
{
name: "Update media",
update: &repository.ProductUpdateParams{
Media: []entity.Media{
{Sort: 1, Type: "image", URL: "https://example.com/1.jpg"},
},
},
validate: func(t *testing.T, updated *entity.Product) {
assert.Len(t, updated.Media, 1)
assert.Equal(t, "image", updated.Media[0].Type)
},
},
{
name: "Update custom fields",
update: &repository.ProductUpdateParams{
CustomFields: []entity.CustomFields{
{Key: "color", Value: "red"},
},
},
validate: func(t *testing.T, updated *entity.Product) {
assert.Len(t, updated.CustomFields, 1)
assert.Equal(t, "color", updated.CustomFields[0].Key)
assert.Equal(t, "red", updated.CustomFields[0].Value)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := model.Update(context.Background(), product.ID.Hex(), tt.update)
assert.NoError(t, err)
// 重新查詢確認資料已更新
updated, err := model.FindOneByID(context.Background(), product.ID.Hex())
assert.NoError(t, err)
tt.validate(t, updated)
})
}
}
func TestProductRepository_Transaction(t *testing.T) {
model, tearDown, err := SetupTestProductRepository("testDB")
defer tearDown()
assert.NoError(t, err)
ctx := context.Background()
tests := []struct {
name string
transactionFunc func(sessCtx mongo.SessionContext) (any, error)
expectError bool
}{
{
name: "Successful Transaction",
transactionFunc: func(sessCtx mongo.SessionContext) (any, error) {
product := &entity.Product{
UID: "user123",
Title: "Test Transaction Product",
Amount: 1000,
IsPublished: true,
}
err := model.Insert(ctx, product)
return product, err
},
expectError: false,
},
{
name: "Transaction Rollback on Error",
transactionFunc: func(sessCtx mongo.SessionContext) (any, error) {
product := &entity.Product{
UID: "user123",
Title: "Rollback Test Product",
Amount: 5000,
IsPublished: true,
}
_ = model.Insert(ctx, product)
// 模擬交易內錯誤
return nil, errors.New("forced error")
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := model.Transaction(ctx, tt.transactionFunc)
if tt.expectError {
assert.Error(t, err, "交易應該返回錯誤")
} else {
assert.NoError(t, err, "交易應該成功完成")
product, _, err := model.ListProduct(ctx,
&repository.ProductQueryParams{
PageSize: 20, PageIndex: 1,
UID: ptr("user123")})
assert.NoError(t, err)
assert.Equal(t, product[0].UID, "user123")
}
})
}
}
func ptr[T any](v T) *T {
return &v
}