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/repository" mgo "code.30cm.net/digimon/library-go/mongo" "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" ) func SetupTestProductStatisticsRepo(db string) (repository.ProductStatisticsRepo, 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 := ProductStatisticsRepositoryParam{ Conf: conf, CacheConf: cacheConf, CacheOpts: cacheOpts, DBOpts: []mon.Option{ mgo.SetCustomDecimalType(), mgo.InitMongoOptions(*conf), }, } repo := NewProductStatisticsRepository(param) _, _ = repo.Index20250317001UP(context.Background()) return repo, tearDown, nil } func TestCreateProductStatistics(t *testing.T) { // 假設有 SetupTestProductStatisticsRepository 可用來建立測試用的 ProductStatisticsRepository repo, tearDown, err := SetupTestProductStatisticsRepo("testDB") require.NoError(t, err) defer tearDown() ctx := context.Background() // 定義多筆測試資料(不包含 ID、CreatedAt、UpdatedAt,由 Create 自動填入) statsList := []*entity.ProductStatistics{ { ProductID: "prod-001", Orders: 100, AverageRating: 4.5, FansCount: 200, }, { ProductID: "prod-002", Orders: 50, AverageRating: 3.8, FansCount: 150, }, } // 逐筆呼叫 Create 新增資料 for _, ps := range statsList { err := repo.Create(ctx, ps) assert.NoError(t, err, "Create should not return error") } // 驗證每筆資料的自動欄位與內容 for _, ps := range statsList { // 檢查 ID 與時間欄位是否有自動填入 assert.False(t, ps.ID.IsZero(), "ID should be generated") assert.NotZero(t, ps.CreatedAt, "CreatedAt should be set") assert.NotZero(t, ps.UpdatedAt, "UpdatedAt should be set") // 查詢 DB 確認資料是否存在且欄位值正確 result, err := repo.GetByID(ctx, ps.ID.Hex()) assert.NoError(t, err, "GetByID should not return error") assert.Equal(t, ps.ProductID, result.ProductID, "ProductID should match") assert.Equal(t, ps.Orders, result.Orders, "Orders should match") assert.Equal(t, ps.AverageRating, result.AverageRating, "AverageRating should match") assert.Equal(t, ps.FansCount, result.FansCount, "FansCount should match") } } func TestGetProductStatisticsByProductID(t *testing.T) { repo, tearDown, err := SetupTestProductStatisticsRepo("testDB") require.NoError(t, err) defer tearDown() ctx := context.Background() stats1 := &entity.ProductStatistics{ ProductID: "prod-001", Orders: 100, AverageRating: 4.5, FansCount: 200, } stats2 := &entity.ProductStatistics{ ProductID: "prod-002", Orders: 50, AverageRating: 3.8, FansCount: 150, } err = repo.Create(ctx, stats1) require.NoError(t, err) err = repo.Create(ctx, stats2) require.NoError(t, err) // 定義 table-driven 測試案例 tests := []struct { name string inputProductID string expectedErr error expectedStatistics *entity.ProductStatistics }{ { name: "record exists - prod-001", inputProductID: "prod-001", expectedErr: nil, expectedStatistics: stats1, }, { name: "record exists - prod-002", inputProductID: "prod-002", expectedErr: nil, expectedStatistics: stats2, }, { name: "record not found", inputProductID: "non-existent", expectedErr: ErrNotFound, }, } for _, tt := range tests { tt := tt // capture range variable t.Run(tt.name, func(t *testing.T) { result, err := repo.GetByProductID(ctx, tt.inputProductID) if tt.expectedErr != nil { require.Error(t, err) assert.Equal(t, tt.expectedErr, err) assert.Nil(t, result) } else { require.NoError(t, err) // 驗證回傳的資料是否符合預期 assert.Equal(t, tt.expectedStatistics.ProductID, result.ProductID) assert.Equal(t, tt.expectedStatistics.Orders, result.Orders) assert.Equal(t, tt.expectedStatistics.AverageRating, result.AverageRating) assert.Equal(t, tt.expectedStatistics.FansCount, result.FansCount) // 其他自動產生欄位如 ID、CreatedAt、UpdatedAt 也可檢查非零 assert.False(t, result.ID.IsZero(), "ID should be generated") assert.NotZero(t, result.CreatedAt, "CreatedAt should be set") assert.NotZero(t, result.UpdatedAt, "UpdatedAt should be set") } }) } } func TestIncOrders(t *testing.T) { repo, tearDown, err := SetupTestProductStatisticsRepo("testDB") require.NoError(t, err) defer tearDown() ctx := context.Background() // 插入一筆測試資料:初始 Orders 為 100 stats := &entity.ProductStatistics{ ProductID: "prod-001", Orders: 100, AverageRating: 4.5, FansCount: 200, } err = repo.Create(ctx, stats) require.NoError(t, err) tests := []struct { name string productID string increment int64 expectedOrders uint64 // 預期的 Orders 值 expectErr bool }{ { name: "Valid increment", productID: "prod-001", increment: 10, expectedOrders: 110, expectErr: false, }, { name: "Non-existent product", productID: "prod-not-exist", increment: 5, // 當產品不存在時,應回傳錯誤,不檢查 Orders expectErr: true, }, } for _, tt := range tests { tt := tt // capture range variable t.Run(tt.name, func(t *testing.T) { err := repo.IncOrders(ctx, tt.productID, tt.increment) if tt.expectErr { require.Error(t, err) // 可進一步檢查錯誤訊息中是否包含特定關鍵字 assert.Contains(t, err.Error(), "failed to get product_id by id") } else { require.NoError(t, err) // 若成功,利用 GetByProductID 取得最新資料,檢查 Orders 是否正確更新 ps, err := repo.GetByProductID(ctx, tt.productID) require.NoError(t, err) assert.Equal(t, tt.expectedOrders, ps.Orders) } }) } } func TestDecOrders(t *testing.T) { repo, tearDown, err := SetupTestProductStatisticsRepo("testDB") require.NoError(t, err) defer tearDown() ctx := context.Background() // 插入一筆測試資料,初始 Orders 為 100 stats := &entity.ProductStatistics{ ProductID: "prod-001", Orders: 100, AverageRating: 4.5, FansCount: 200, } err = repo.Create(ctx, stats) require.NoError(t, err) tests := []struct { name string productID string decrement int64 expectedOrders uint64 // 減少成功後預期的 Orders 值 expectErr bool }{ { name: "Valid decrease", productID: "prod-001", decrement: 30, expectedOrders: 70, // 100 - 30 expectErr: false, }, { name: "Insufficient orders", productID: "prod-001", decrement: 150, // 超過現有數量 expectedOrders: 70, // 100 - 30 expectErr: false, }, { name: "Non-existent product", productID: "prod-not-exist", decrement: 10, expectErr: true, }, } for _, tt := range tests { tt := tt // capture range variable t.Run(tt.name, func(t *testing.T) { err := repo.DecOrders(ctx, tt.productID, tt.decrement) if tt.expectErr { require.Error(t, err) } else { require.NoError(t, err) // 成功減少後,利用 GetByProductID 取得最新資料 updated, err := repo.GetByProductID(ctx, tt.productID) require.NoError(t, err) assert.Equal(t, tt.expectedOrders, updated.Orders) } }) } } func TestUpdateAverageRating(t *testing.T) { repo, tearDown, err := SetupTestProductStatisticsRepo("testDB") require.NoError(t, err) defer tearDown() ctx := context.Background() // 插入一筆測試資料,初始 AverageRating 為 4.0 stats := &entity.ProductStatistics{ ProductID: "prod-001", Orders: 100, AverageRating: 4.0, FansCount: 50, } err = repo.Create(ctx, stats) require.NoError(t, err) tests := []struct { name string productID string newRating float64 expectErr bool expectedValue float64 // 預期更新後的 AverageRating }{ { name: "Update existing product", productID: "prod-001", newRating: 4.8, expectErr: false, expectedValue: 4.8, }, { name: "Non-existent product", productID: "prod-nonexist", newRating: 3.5, expectErr: true, }, } for _, tt := range tests { tt := tt // capture range variable t.Run(tt.name, func(t *testing.T) { err := repo.UpdateAverageRating(ctx, tt.productID, tt.newRating) if tt.expectErr { require.Error(t, err) } else { require.NoError(t, err) // 取得更新後的資料 updated, err := repo.GetByProductID(ctx, tt.productID) require.NoError(t, err) assert.Equal(t, tt.expectedValue, updated.AverageRating, "AverageRating should be updated") // 驗證更新時間不為 0 assert.NotZero(t, updated.AverageRatingUpdateTime, "AverageRatingUpdateTime should be set") assert.NotZero(t, updated.UpdatedAt, "UpdatedAt should be set") } }) } } func TestIncFansCount(t *testing.T) { repo, tearDown, err := SetupTestProductStatisticsRepo("testDB") require.NoError(t, err) defer tearDown() ctx := context.Background() // 插入一筆測試資料,初始 FansCount 為 200 stats := &entity.ProductStatistics{ ProductID: "prod-001", Orders: 100, AverageRating: 4.0, FansCount: 200, } err = repo.Create(ctx, stats) require.NoError(t, err) tests := []struct { name string productID string incCount uint64 expectedCount uint64 // 預期更新後的 FansCount expectErr bool }{ { name: "Valid increment", productID: "prod-001", incCount: 50, expectedCount: 250, // 200 + 50 expectErr: false, }, { name: "Non-existent product", productID: "non-existent", incCount: 10, expectErr: true, }, } for _, tt := range tests { tt := tt // capture range variable t.Run(tt.name, func(t *testing.T) { err := repo.IncFansCount(ctx, tt.productID, tt.incCount) if tt.expectErr { require.Error(t, err) } else { require.NoError(t, err) // 取得更新後的資料,驗證 FansCount 是否正確更新 updated, err := repo.GetByProductID(ctx, tt.productID) require.NoError(t, err) assert.Equal(t, tt.expectedCount, updated.FansCount, "FansCount should be incremented correctly") assert.NotZero(t, updated.FansCountUpdateTime, "FansCountUpdateTime should be set") assert.NotZero(t, updated.UpdatedAt, "UpdatedAt should be set") } }) } } func TestDecFansCount(t *testing.T) { repo, tearDown, err := SetupTestProductStatisticsRepo("testDB") require.NoError(t, err) defer tearDown() ctx := context.Background() // 先建立兩筆測試資料 // 測試 valid case:初始 FansCount 為 200 statsValid := &entity.ProductStatistics{ ProductID: "prod-valid", Orders: 100, AverageRating: 4.5, FansCount: 200, } err = repo.Create(ctx, statsValid) require.NoError(t, err) // 測試 insufficient case:初始 FansCount 為 30 statsInsufficient := &entity.ProductStatistics{ ProductID: "prod-insufficient", Orders: 50, AverageRating: 3.8, FansCount: 30, } err = repo.Create(ctx, statsInsufficient) require.NoError(t, err) tests := []struct { name string productID string decrement uint64 expectedFans uint64 // 預期更新後的 FansCount (僅 valid case) expectErr bool }{ { name: "Valid decrement", productID: "prod-valid", decrement: 50, expectedFans: 150, // 200 - 50 expectErr: false, }, { name: "Insufficient fans", productID: "prod-insufficient", decrement: 50, expectedFans: 30, // 扣超過就跟原本一樣不會變 expectErr: false, }, { name: "Non-existent product", productID: "prod-nonexistent", decrement: 10, expectErr: true, }, } for _, tt := range tests { tt := tt // capture range variable t.Run(tt.name, func(t *testing.T) { err := repo.DecFansCount(ctx, tt.productID, tt.decrement) if tt.expectErr { require.Error(t, err) } else { require.NoError(t, err) // 取得更新後的資料,驗證 FansCount 是否正確更新 updated, err := repo.GetByProductID(ctx, tt.productID) require.NoError(t, err) assert.Equal(t, tt.expectedFans, updated.FansCount, "FansCount should be decremented correctly") } }) } } func TestDeleteProductStatistics(t *testing.T) { repo, tearDown, err := SetupTestProductStatisticsRepo("testDB") require.NoError(t, err) defer tearDown() ctx := context.Background() // 插入一筆測試資料 stats := &entity.ProductStatistics{ ProductID: "prod-001", Orders: 100, AverageRating: 4.5, FansCount: 200, } err = repo.Create(ctx, stats) require.NoError(t, err) tests := []struct { name string id string expectErr error // 預期錯誤 checkDelete bool // 若為 true,刪除成功後進行資料查詢驗證 }{ { name: "Delete existing record", id: stats.ID.Hex(), expectErr: nil, checkDelete: true, }, { name: "Invalid ObjectID format", id: "invalid-id", expectErr: ErrInvalidObjectID, checkDelete: false, }, } for _, tc := range tests { tc := tc // capture range variable t.Run(tc.name, func(t *testing.T) { err := repo.Delete(ctx, tc.id) if tc.expectErr != nil { require.Error(t, err) assert.Equal(t, tc.expectErr, err) } else { require.NoError(t, err) if tc.checkDelete { // 刪除成功後,透過 GetByID 應查無資料 _, err := repo.GetByID(ctx, tc.id) require.Error(t, err) assert.Equal(t, ErrNotFound, err) } } }) } }