package usecase import ( "context" "errors" "testing" "time" "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity" "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository" "code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/usecase" mockRepository "code.30cm.net/digimon/app-cloudep-product-service/pkg/mock/repository" repo "code.30cm.net/digimon/app-cloudep-product-service/pkg/repository" "github.com/stretchr/testify/assert" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.uber.org/mock/gomock" ) func TestProductUseCase_Create(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockProductRepo := mockRepository.NewMockProductRepository(mockCtrl) mockStatsRepo := mockRepository.NewMockProductStatisticsRepo(mockCtrl) mockTagBinding := mockRepository.NewMockTagBindingRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductRepo: mockProductRepo, ProductStatisticsRepo: mockStatsRepo, TagBinding: mockTagBinding, }) ctx := context.Background() input := &usecase.Product{ UID: ptr("user-1"), Title: ptr("Test Product"), ShortTitle: ptr("Short"), Details: ptr("Details here"), ShortDescription: ptr("Desc"), Media: []usecase.Media{{Sort: 1, Type: "image", URL: "http://image"}}, Slug: ptr("test-product"), IsPublished: ptr(true), Amount: 1000, StartTime: ptr("2025-04-04T00:00:00Z"), EndTime: ptr("2025-04-10T00:00:00Z"), Category: ptr("education"), CustomFields: []usecase.CustomFields{{Key: "Level", Value: "Beginner"}}, Tags: []string{"t1", "t2"}, } tests := []struct { name string mockSetup func() expectedError string }{ { name: "建立成功", mockSetup: func() { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { // ✅ 這邊用 gomock.Any() 模擬 sessCtx mockProductRepo.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil) mockStatsRepo.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil) mockTagBinding.EXPECT().BindTags(gomock.Any(), gomock.Any()).Return(nil) _, err := fn(nil) return err }) }, }, { name: "插入失敗", mockSetup: func() { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("failed to create product")) _, err := fn(nil) return err }) }, expectedError: "failed to create product", }, { name: "統計建立失敗", mockSetup: func() { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil) mockStatsRepo.EXPECT().Create(gomock.Any(), gomock.Any()).Return(errors.New("failed to create product statistics")) _, err := fn(nil) return err }) }, expectedError: "failed to create product statistics", }, { name: "綁定標籤失敗", mockSetup: func() { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil) mockStatsRepo.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil) mockTagBinding.EXPECT().BindTags(gomock.Any(), gomock.Any()).Return(errors.New("failed to bind product tags")) _, err := fn(nil) return err }) }, expectedError: "failed to bind product tags", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.mockSetup() err := useCase.Create(ctx, input) if tt.expectedError == "" { assert.NoError(t, err) } else { assert.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) } }) } } func TestProductUseCase_IncOrders(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockStatsRepo := mockRepository.NewMockProductStatisticsRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductStatisticsRepo: mockStatsRepo, }) ctx := context.Background() productID := primitive.NewObjectID().Hex() t.Run("成功增加訂單", func(t *testing.T) { mockStatsRepo.EXPECT().IncOrders(ctx, productID, int64(2)).Return(nil) err := useCase.IncOrders(ctx, productID, 2) assert.NoError(t, err) }) t.Run("資料庫錯誤", func(t *testing.T) { mockStatsRepo.EXPECT().IncOrders(ctx, productID, int64(1)).Return(errors.New("db error")) err := useCase.IncOrders(ctx, productID, 1) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to inc order") }) } func TestProductUseCase_DecOrders(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockStatsRepo := mockRepository.NewMockProductStatisticsRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductStatisticsRepo: mockStatsRepo, }) ctx := context.Background() productID := primitive.NewObjectID().Hex() t.Run("成功減少訂單", func(t *testing.T) { mockStatsRepo.EXPECT().DecOrders(ctx, productID, int64(3)).Return(nil) err := useCase.DecOrders(ctx, productID, 3) assert.NoError(t, err) }) t.Run("資料庫錯誤", func(t *testing.T) { mockStatsRepo.EXPECT().DecOrders(ctx, productID, int64(1)).Return(errors.New("db error")) err := useCase.DecOrders(ctx, productID, 1) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to dec order") }) } func TestProductUseCase_UpdateAverageRating(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockStatsRepo := mockRepository.NewMockProductStatisticsRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductStatisticsRepo: mockStatsRepo, }) ctx := context.Background() productID := primitive.NewObjectID().Hex() t.Run("成功更新評價", func(t *testing.T) { mockStatsRepo.EXPECT().UpdateAverageRating(ctx, productID, 4.5).Return(nil) err := useCase.UpdateAverageRating(ctx, productID, 4.5) assert.NoError(t, err) }) t.Run("資料庫錯誤", func(t *testing.T) { mockStatsRepo.EXPECT().UpdateAverageRating(ctx, productID, 3.0).Return(errors.New("db error")) err := useCase.UpdateAverageRating(ctx, productID, 3.0) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to update average rating") }) } func TestProductUseCase_IncFansCount(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockStatsRepo := mockRepository.NewMockProductStatisticsRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductStatisticsRepo: mockStatsRepo, }) ctx := context.Background() productID := primitive.NewObjectID().Hex() t.Run("成功增加追蹤人數", func(t *testing.T) { mockStatsRepo.EXPECT().IncFansCount(ctx, productID, uint64(10)).Return(nil) err := useCase.IncFansCount(ctx, productID, 10) assert.NoError(t, err) }) t.Run("資料庫錯誤", func(t *testing.T) { mockStatsRepo.EXPECT().IncFansCount(ctx, productID, uint64(5)).Return(errors.New("db error")) err := useCase.IncFansCount(ctx, productID, 5) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to inc fans count") }) } func TestProductUseCase_DecFansCount(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockStatsRepo := mockRepository.NewMockProductStatisticsRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductStatisticsRepo: mockStatsRepo, }) ctx := context.Background() productID := primitive.NewObjectID().Hex() t.Run("成功減少追蹤人數", func(t *testing.T) { mockStatsRepo.EXPECT().DecFansCount(ctx, productID, uint64(7)).Return(nil) err := useCase.DecFansCount(ctx, productID, 7) assert.NoError(t, err) }) t.Run("資料庫錯誤", func(t *testing.T) { mockStatsRepo.EXPECT().DecFansCount(ctx, productID, uint64(2)).Return(errors.New("db error")) err := useCase.DecFansCount(ctx, productID, 2) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to dec fans count") }) } func TestProductUseCase_Update(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockProductRepo := mockRepository.NewMockProductRepository(mockCtrl) mockTagBinding := mockRepository.NewMockTagBindingRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductRepo: mockProductRepo, TagBinding: mockTagBinding, }) ctx := context.Background() id := primitive.NewObjectID().Hex() product := &usecase.Product{ Title: ptr("Updated Product"), Tags: []string{"t1", "t2"}, } t.Run("更新成功", func(t *testing.T) { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Update(gomock.Any(), id, gomock.Any()).Return(nil, nil) mockTagBinding.EXPECT().UnbindTagByReferenceID(gomock.Any(), id).Return(nil) mockTagBinding.EXPECT().BindTags(gomock.Any(), gomock.Any()).Return(nil) _, err := fn(nil) return err }, ) err := useCase.Update(ctx, id, product) assert.NoError(t, err) }) t.Run("更新失敗 - ProductRepo", func(t *testing.T) { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Update(gomock.Any(), id, gomock.Any()).Return(nil, errors.New("update failed")) _, err := fn(nil) return err }, ) err := useCase.Update(ctx, id, product) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to update product") }) t.Run("解除綁定失敗", func(t *testing.T) { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Update(gomock.Any(), id, gomock.Any()).Return(nil, nil) mockTagBinding.EXPECT().UnbindTagByReferenceID(gomock.Any(), id).Return(errors.New("unbind error")) _, err := fn(nil) return err }, ) err := useCase.Update(ctx, id, product) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to unbind tags") }) t.Run("綁定失敗", func(t *testing.T) { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Update(gomock.Any(), id, gomock.Any()).Return(nil, nil) mockTagBinding.EXPECT().UnbindTagByReferenceID(gomock.Any(), id).Return(nil) mockTagBinding.EXPECT().BindTags(gomock.Any(), gomock.Any()).Return(errors.New("bind error")) _, err := fn(nil) return err }, ) err := useCase.Update(ctx, id, product) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to bind tags") }) } // ========================= Delete ========================= func TestProductUseCase_Delete(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockProductRepo := mockRepository.NewMockProductRepository(mockCtrl) mockTagRepo := mockRepository.NewMockTagRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductRepo: mockProductRepo, TagRepo: mockTagRepo, }) ctx := context.Background() id := primitive.NewObjectID().Hex() t.Run("刪除成功", func(t *testing.T) { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Delete(gomock.Any(), id).Return(nil) mockTagRepo.EXPECT().UnbindTagByReferenceID(gomock.Any(), id).Return(nil) _, err := fn(nil) return err }, ) err := useCase.Delete(ctx, id) assert.NoError(t, err) }) t.Run("刪除失敗", func(t *testing.T) { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Delete(gomock.Any(), id).Return(errors.New("delete error")) _, err := fn(nil) return err }, ) err := useCase.Delete(ctx, id) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to delete product") }) t.Run("解除綁定失敗", func(t *testing.T) { mockProductRepo.EXPECT().Transaction(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, fn func(sessCtx mongo.SessionContext) (any, error), opts ...*options.TransactionOptions) error { mockProductRepo.EXPECT().Delete(gomock.Any(), id).Return(nil) mockTagRepo.EXPECT().UnbindTagByReferenceID(gomock.Any(), id).Return(errors.New("unbind tag error")) _, err := fn(nil) return err }, ) err := useCase.Delete(ctx, id) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to unbind tags") }) } // ========================= Get ========================= func TestProductUseCase_Get(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockProductRepo := mockRepository.NewMockProductRepository(mockCtrl) mockStatsRepo := mockRepository.NewMockProductStatisticsRepo(mockCtrl) mockTagRepo := mockRepository.NewMockTagRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductRepo: mockProductRepo, ProductStatisticsRepo: mockStatsRepo, TagRepo: mockTagRepo, }) ctx := context.Background() id := primitive.NewObjectID() t.Run("查詢成功", func(t *testing.T) { mockProductRepo.EXPECT().FindOneByID(ctx, id.Hex()).Return(&entity.Product{ ID: id, UID: "u1", Title: "test", IsPublished: true, Media: []entity.Media{}, CustomFields: []entity.CustomFields{}, CreatedAt: time.Now().Unix(), UpdatedAt: time.Now().Unix(), }, nil) mockStatsRepo.EXPECT().GetByID(ctx, id.Hex()).Return(&entity.ProductStatistics{}, nil) mockTagRepo.EXPECT().GetBindingsByReference(ctx, id.Hex()).Return([]*entity.TagsBindingTable{}, nil) mockTagRepo.EXPECT().GetByIDs(ctx, gomock.Any()).Return([]*entity.Tags{}, nil) resp, err := useCase.Get(ctx, id.Hex()) assert.NoError(t, err) assert.Equal(t, "u1", resp.UID) }) t.Run("查詢失敗 - ProductRepo", func(t *testing.T) { mockProductRepo.EXPECT().FindOneByID(ctx, id.Hex()).Return(nil, errors.New("db error")) resp, err := useCase.Get(ctx, id.Hex()) assert.Error(t, err) assert.Nil(t, resp) }) } // ========================= Tag Binding ========================= func TestProductUseCase_BindTag(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockProductRepo := mockRepository.NewMockProductRepository(mockCtrl) mockTagRepo := mockRepository.NewMockTagRepo(mockCtrl) mockTagBinding := mockRepository.NewMockTagBindingRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductRepo: mockProductRepo, TagRepo: mockTagRepo, TagBinding: mockTagBinding, }) ctx := context.Background() binding := usecase.TagsBindingTable{ ReferenceID: "pid-123", TagID: "tag-123", } t.Run("成功綁定", func(t *testing.T) { mockTagRepo.EXPECT().GetByID(ctx, binding.TagID).Return(&entity.Tags{}, nil) mockProductRepo.EXPECT().FindOneByID(ctx, binding.ReferenceID).Return(&entity.Product{}, nil) mockTagBinding.EXPECT().BindTags(ctx, gomock.Any()).Return(nil) err := useCase.BindTag(ctx, binding) assert.NoError(t, err) }) t.Run("Tag 不存在", func(t *testing.T) { mockTagRepo.EXPECT().GetByID(ctx, binding.TagID).Return(nil, repo.ErrNotFound) err := useCase.BindTag(ctx, binding) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to get tags") }) t.Run("Product 不存在", func(t *testing.T) { mockTagRepo.EXPECT().GetByID(ctx, binding.TagID).Return(&entity.Tags{}, nil) mockProductRepo.EXPECT().FindOneByID(ctx, binding.ReferenceID).Return(nil, repo.ErrNotFound) err := useCase.BindTag(ctx, binding) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to get tags") }) t.Run("綁定錯誤", func(t *testing.T) { mockTagRepo.EXPECT().GetByID(ctx, binding.TagID).Return(&entity.Tags{}, nil) mockProductRepo.EXPECT().FindOneByID(ctx, binding.ReferenceID).Return(&entity.Product{}, nil) mockTagBinding.EXPECT().BindTags(ctx, gomock.Any()).Return(errors.New("db error")) err := useCase.BindTag(ctx, binding) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to bind tags") }) } func TestProductUseCase_UnbindTag(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockProductRepo := mockRepository.NewMockProductRepository(mockCtrl) mockTagRepo := mockRepository.NewMockTagRepo(mockCtrl) mockTagBinding := mockRepository.NewMockTagBindingRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductRepo: mockProductRepo, TagRepo: mockTagRepo, TagBinding: mockTagBinding, }) ctx := context.Background() binding := usecase.TagsBindingTable{ ReferenceID: "pid-123", TagID: "tag-123", } t.Run("成功解除綁定", func(t *testing.T) { mockTagRepo.EXPECT().GetByID(ctx, binding.TagID).Return(&entity.Tags{}, nil) mockProductRepo.EXPECT().FindOneByID(ctx, binding.ReferenceID).Return(&entity.Product{}, nil) mockTagBinding.EXPECT().UnbindTag(ctx, binding.TagID, binding.ReferenceID).Return(nil) err := useCase.UnbindTag(ctx, binding) assert.NoError(t, err) }) t.Run("解除失敗", func(t *testing.T) { mockTagRepo.EXPECT().GetByID(ctx, binding.TagID).Return(&entity.Tags{}, nil) mockProductRepo.EXPECT().FindOneByID(ctx, binding.ReferenceID).Return(&entity.Product{}, nil) mockTagBinding.EXPECT().UnbindTag(ctx, binding.TagID, binding.ReferenceID).Return(errors.New("unbind error")) err := useCase.UnbindTag(ctx, binding) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to unbind tags") }) } func TestProductUseCase_GetBindingsByReference(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockTagBinding := mockRepository.NewMockTagBindingRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ TagBinding: mockTagBinding, }) ctx := context.Background() refID := "ref-123" t.Run("查詢成功", func(t *testing.T) { now := time.Now().Unix() mockTagBinding.EXPECT().GetBindingsByReference(ctx, refID).Return([]*entity.TagsBindingTable{ { ID: primitive.NewObjectID(), ReferenceID: refID, TagID: "tag-1", CreatedAt: now, UpdatedAt: now, }, }, nil) resp, err := useCase.GetBindingsByReference(ctx, refID) assert.NoError(t, err) assert.Len(t, resp, 1) }) t.Run("查詢失敗", func(t *testing.T) { mockTagBinding.EXPECT().GetBindingsByReference(ctx, refID).Return(nil, errors.New("db error")) resp, err := useCase.GetBindingsByReference(ctx, refID) assert.Error(t, err) assert.Nil(t, resp) }) } func TestProductUseCase_ListTagBinding(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockTagBinding := mockRepository.NewMockTagBindingRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ TagBinding: mockTagBinding, }) ctx := context.Background() params := usecase.TagBindingQueryParams{ ReferenceID: ptr("r1"), TagID: ptr("t1"), PageSize: 10, PageIndex: 1, } t.Run("查詢成功", func(t *testing.T) { now := time.Now().Unix() mockTagBinding.EXPECT().ListTagBinding(ctx, repository.TagBindingQueryParams{ ReferenceID: params.ReferenceID, TagID: params.TagID, PageSize: params.PageSize, PageIndex: params.PageIndex, }).Return([]*entity.TagsBindingTable{ { ID: primitive.NewObjectID(), ReferenceID: *params.ReferenceID, TagID: *params.TagID, CreatedAt: now, UpdatedAt: now, }, }, int64(1), nil) resp, total, err := useCase.ListTagBinding(ctx, params) assert.NoError(t, err) assert.Len(t, resp, 1) assert.Equal(t, int64(1), total) }) t.Run("查詢失敗", func(t *testing.T) { mockTagBinding.EXPECT().ListTagBinding(ctx, repository.TagBindingQueryParams{ ReferenceID: params.ReferenceID, TagID: params.TagID, PageSize: params.PageSize, PageIndex: params.PageIndex, }).Return(nil, int64(0), errors.New("db error")) resp, total, err := useCase.ListTagBinding(ctx, params) assert.Error(t, err) assert.Nil(t, resp) assert.Equal(t, int64(0), total) }) } // ========================= List ========================= func TestProductUseCase_List(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockProductRepo := mockRepository.NewMockProductRepository(mockCtrl) mockStatsRepo := mockRepository.NewMockProductStatisticsRepo(mockCtrl) mockTagRepo := mockRepository.NewMockTagRepo(mockCtrl) useCase := MustProductUseCase(ProductUseCaseParam{ ProductRepo: mockProductRepo, ProductStatisticsRepo: mockStatsRepo, TagRepo: mockTagRepo, }) ctx := context.Background() id := primitive.NewObjectID() mockProductRepo.EXPECT().ListProduct(ctx, &repository.ProductQueryParams{ PageIndex: 1, PageSize: 10, }).Return([]*entity.Product{{ID: id, UID: "u1", Title: "T", CreatedAt: time.Now().Unix(), UpdatedAt: time.Now().Unix()}}, int64(1), nil) mockStatsRepo.EXPECT().GetByID(ctx, id.Hex()).Return(&entity.ProductStatistics{}, nil) mockTagRepo.EXPECT().GetBindingsByReference(ctx, id.Hex()).Return([]*entity.TagsBindingTable{}, nil) mockTagRepo.EXPECT().GetByIDs(ctx, gomock.Any()).Return([]*entity.Tags{}, nil) list, count, err := useCase.List(ctx, usecase.ProductQueryParams{ PageIndex: 1, PageSize: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), count) assert.Len(t, list, 1) } // 工具:指標轉換 func ptr[T any](v T) *T { return &v }