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() 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 }