feat: usecast test

This commit is contained in:
王性驊 2025-04-04 17:33:48 +08:00
parent 39841cc168
commit 52905207c6
6 changed files with 1749 additions and 2 deletions

1
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/testcontainers/testcontainers-go v0.34.0
github.com/zeromicro/go-zero v1.8.1
go.mongodb.org/mongo-driver v1.17.3
go.uber.org/mock v0.5.0
google.golang.org/grpc v1.71.0
google.golang.org/protobuf v1.36.5
)

6
go.sum
View File

@ -278,6 +278,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
@ -348,8 +350,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

138
pkg/usecase/category.go Normal file
View File

@ -0,0 +1,138 @@
package usecase
import (
"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"
"code.30cm.net/digimon/library-go/errs"
"context"
"errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/mon"
)
type CategoryUseCaseParam struct {
CategoryRepo repository.CategoryRepository
}
type CategoryUseCase struct {
CategoryUseCaseParam
}
func MustCategoryUseCase(param CategoryUseCaseParam) usecase.CategoryUseCase {
return &CategoryUseCase{
param,
}
}
func (use *CategoryUseCase) Insert(ctx context.Context, data *entity.Category) error {
err := use.CategoryRepo.Insert(ctx, &entity.Category{
Name: data.Name,
})
if err != nil {
return errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: data},
{Key: "func", Value: "CategoryRepo.Insert"},
{Key: "err", Value: err.Error()},
},
"failed to create category").Wrap(err)
}
return nil
}
func (use *CategoryUseCase) FindOneByID(ctx context.Context, id string) (*entity.Category, error) {
res, err := use.CategoryRepo.FindOneByID(ctx, id)
if err != nil {
if errors.Is(err, mon.ErrNotFound) {
e := errs.ResourceNotFound(
"failed to get category",
)
return nil, e
}
e := errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: id},
{Key: "func", Value: "CategoryRepo.FindOneByID"},
{Key: "err", Value: err.Error()},
},
"failed to get category").Wrap(err)
return nil, e
}
return res, nil
}
func (use *CategoryUseCase) Update(ctx context.Context, id string, data *entity.Category) error {
_, err := use.CategoryRepo.Update(ctx, id, &entity.Category{
Name: data.Name,
})
if err != nil {
if errors.Is(err, mon.ErrNotFound) {
e := errs.ResourceNotFound(
"failed to update category",
)
return e
}
e := errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: id},
{Key: "func", Value: "CategoryRepo.Update"},
{Key: "err", Value: err.Error()},
},
"failed to update category").Wrap(err)
return e
}
return nil
}
func (use *CategoryUseCase) Delete(ctx context.Context, id string) error {
_, err := use.CategoryRepo.Delete(ctx, id)
if err != nil {
e := errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: id},
{Key: "func", Value: "CategoryRepo.Delete"},
{Key: "err", Value: err.Error()},
},
"failed to delete category").Wrap(err)
return e
}
return nil
}
func (use *CategoryUseCase) ListCategory(ctx context.Context, params usecase.CategoryQueryParams) ([]*entity.Category, int64, error) {
category, i, err := use.CategoryRepo.ListCategory(ctx, &repository.CategoryQueryParams{
ID: params.ID,
PageIndex: params.PageIndex,
PageSize: params.PageSize,
})
if err != nil {
e := errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: params},
{Key: "func", Value: "CategoryRepo.ListCategory"},
{Key: "err", Value: err.Error()},
},
"failed to list category").Wrap(err)
return nil, 0, e
}
return category, i, nil
}

View File

@ -0,0 +1,414 @@
package usecase
import (
"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"
"code.30cm.net/digimon/library-go/errs"
"context"
"errors"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/mock/gomock"
"testing"
mockRepository "code.30cm.net/digimon/app-cloudep-product-service/pkg/mock/repository"
)
func TestCategoryUseCase_FindOneByID(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockCategoryRepo := mockRepository.NewMockCategoryRepository(mockCtrl)
useCase := MustCategoryUseCase(CategoryUseCaseParam{
CategoryRepo: mockCategoryRepo,
})
ctx := context.Background()
id := primitive.NewObjectID()
tests := []struct {
name string
id primitive.ObjectID
mockSetup func()
expectedError error
expectedResult *entity.Category
}{
{
name: "成功找到 Category",
id: id,
mockSetup: func() {
mockCategoryRepo.EXPECT().FindOneByID(ctx, id.Hex()).Return(&entity.Category{
ID: id,
Name: "Test Category",
}, nil)
},
expectedError: nil,
expectedResult: &entity.Category{
ID: id,
Name: "Test Category",
},
},
{
name: "Category 不存在",
id: primitive.NewObjectID(),
mockSetup: func() {
mockCategoryRepo.EXPECT().FindOneByID(ctx, gomock.Any()).Return(nil, mon.ErrNotFound)
},
expectedError: errs.ResourceNotFound("failed to get category"),
expectedResult: nil,
},
{
name: "資料庫錯誤",
id: primitive.NewObjectID(),
mockSetup: func() {
mockCategoryRepo.EXPECT().FindOneByID(ctx, gomock.Any()).Return(nil, errors.New("database error"))
},
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: "db_error_id"},
{Key: "func", Value: "CategoryRepo.FindOneByID"},
{Key: "err", Value: "database error"},
},
"failed to get category").Wrap(errors.New("database error")),
expectedResult: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
result, err := useCase.FindOneByID(ctx, tt.id.Hex())
if tt.expectedError == nil {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, result)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
assert.Nil(t, result)
}
})
}
}
func TestCategoryUseCase_Update(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockCategoryRepo := mockRepository.NewMockCategoryRepository(mockCtrl)
useCase := MustCategoryUseCase(CategoryUseCaseParam{
CategoryRepo: mockCategoryRepo,
})
ctx := context.Background()
tests := []struct {
name string
id string
data entity.Category
mockSetup func()
expectedError error
}{
{
name: "成功更新 Category",
id: "valid_id",
data: entity.Category{
Name: "Updated Category",
},
mockSetup: func() {
mockCategoryRepo.EXPECT().Update(ctx, "valid_id", &entity.Category{
Name: "Updated Category",
}).Return(nil, nil)
},
expectedError: nil,
},
{
name: "Category 不存在",
id: "non_existing_id",
data: entity.Category{
Name: "Non-existing Category",
},
mockSetup: func() {
mockCategoryRepo.EXPECT().Update(ctx, "non_existing_id", &entity.Category{
Name: "Non-existing Category",
}).Return(nil, mon.ErrNotFound)
},
expectedError: errs.ResourceNotFound(
"failed to update category",
),
},
{
name: "資料庫錯誤",
id: "db_error_id",
data: entity.Category{
Name: "DB Error Category",
},
mockSetup: func() {
mockCategoryRepo.EXPECT().Update(ctx, "db_error_id", &entity.Category{
Name: "DB Error Category",
}).Return(nil, errors.New("database error"))
},
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: "db_error_id"},
{Key: "func", Value: "CategoryRepo.Update"},
{Key: "err", Value: "database error"},
},
"failed to update category").Wrap(errors.New("database error")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
// 執行測試方法
err := useCase.Update(ctx, tt.id, &tt.data)
// 驗證結果
if tt.expectedError == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
}
})
}
}
func TestCategoryUseCase_Insert(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockCategoryRepo := mockRepository.NewMockCategoryRepository(mockCtrl)
useCase := MustCategoryUseCase(CategoryUseCaseParam{
CategoryRepo: mockCategoryRepo,
})
ctx := context.Background()
tests := []struct {
name string
input entity.Category
mockSetup func()
expectedError error
}{
{
name: "成功插入 Category",
input: entity.Category{
Name: "Test Category",
},
mockSetup: func() {
mockCategoryRepo.EXPECT().Insert(ctx, &entity.Category{
Name: "Test Category",
}).Return(nil)
},
expectedError: nil,
},
{
name: "插入失敗 - Database Error",
input: entity.Category{
Name: "Invalid Category",
},
mockSetup: func() {
mockCategoryRepo.EXPECT().Insert(ctx, &entity.Category{
Name: "Invalid Category",
}).Return(errors.New("database connection error"))
},
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: entity.Category{Name: "Invalid Category"}},
{Key: "func", Value: "CategoryRepo.Insert"},
{Key: "err", Value: "database connection error"},
},
"failed to create category",
).Wrap(errors.New("database connection error")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
// 執行測試方法
err := useCase.Insert(ctx, &tt.input)
// 驗證結果
if tt.expectedError == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
}
})
}
}
func TestCategoryUseCase_Delete(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockCategoryRepo := mockRepository.NewMockCategoryRepository(mockCtrl)
useCase := MustCategoryUseCase(CategoryUseCaseParam{
CategoryRepo: mockCategoryRepo,
})
ctx := context.Background()
tests := []struct {
name string
id string
mockSetup func()
expectedError error
}{
{
name: "成功刪除 Category",
id: "valid_id",
mockSetup: func() {
mockCategoryRepo.EXPECT().Delete(ctx, gomock.Any()).Return(int64(1), nil)
},
expectedError: nil,
},
{
name: "資料庫錯誤",
id: "db_error_id",
mockSetup: func() {
mockCategoryRepo.EXPECT().Delete(ctx, gomock.Any()).Return(int64(0), errors.New("database error"))
},
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: "db_error_id"},
{Key: "func", Value: "CategoryRepo.Delete"},
{Key: "err", Value: "database error"},
},
"failed to delete category").Wrap(errors.New("database error")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
// 執行測試方法
err := useCase.Delete(ctx, tt.id)
// 驗證結果
if tt.expectedError == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
}
})
}
}
func TestCategoryUseCase_ListCategory(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockCategoryRepo := mockRepository.NewMockCategoryRepository(mockCtrl)
useCase := MustCategoryUseCase(CategoryUseCaseParam{
CategoryRepo: mockCategoryRepo,
})
ctx := context.Background()
id := primitive.NewObjectID()
tests := []struct {
name string
params usecase.CategoryQueryParams
mockSetup func()
expectedResult []*entity.Category
expectedCount int64
expectedError error
}{
{
name: "成功列出 Category",
params: usecase.CategoryQueryParams{
ID: []string{id.Hex()},
PageIndex: 1,
PageSize: 10,
},
mockSetup: func() {
mockCategoryRepo.EXPECT().ListCategory(ctx, &repository.CategoryQueryParams{
ID: []string{id.Hex()},
PageIndex: 1,
PageSize: 10,
}).Return([]*entity.Category{
{
ID: id,
Name: "Test Category",
},
}, int64(1), nil)
},
expectedResult: []*entity.Category{
{
ID: id,
Name: "Test Category",
},
},
expectedCount: int64(1),
expectedError: nil,
},
{
name: "資料庫錯誤",
params: usecase.CategoryQueryParams{
ID: []string{"db_error_id"},
PageIndex: 1,
PageSize: 10,
},
mockSetup: func() {
mockCategoryRepo.EXPECT().ListCategory(ctx, &repository.CategoryQueryParams{
ID: []string{"db_error_id"},
PageIndex: 1,
PageSize: 10,
}).Return(nil, int64(0), errors.New("database error"))
},
expectedResult: nil,
expectedCount: int64(0),
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "req", Value: usecase.CategoryQueryParams{
ID: []string{"db_error_id"},
PageIndex: 1,
PageSize: 10,
}},
{Key: "func", Value: "CategoryRepo.ListCategory"},
{Key: "err", Value: "database error"},
},
"failed to list category").Wrap(errors.New("database error")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
// 執行測試方法
result, count, err := useCase.ListCategory(ctx, tt.params)
// 驗證結果
assert.Equal(t, tt.expectedResult, result)
assert.Equal(t, tt.expectedCount, count)
if tt.expectedError == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
}
})
}
}

552
pkg/usecase/kyc_test.go Normal file
View File

@ -0,0 +1,552 @@
package usecase
import (
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity"
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/kyc"
"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"
"code.30cm.net/digimon/library-go/errs"
"context"
"errors"
"fmt"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/proto"
"testing"
)
func TestKYCUseCase_Create(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockKYCRepo := mockRepository.NewMockKYCRepository(mockCtrl)
useCase := MustKYCUseCase(KYCUseCaseParam{
KYCRepo: mockKYCRepo,
})
ctx := context.Background()
uid := "test-user"
tests := []struct {
name string
input *entity.KYC
mockSetup func()
expectedError error
}{
{
name: "查不到資料可以建立",
input: &entity.KYC{
UID: uid,
},
mockSetup: func() {
mockKYCRepo.EXPECT().FindLatestByUID(ctx, uid).Return(nil, repo.ErrNotFound)
mockKYCRepo.EXPECT().Create(ctx, gomock.Any()).Return(nil)
},
expectedError: nil,
},
{
name: "前一筆為 REJECTED 可建立",
input: &entity.KYC{
UID: uid,
},
mockSetup: func() {
mockKYCRepo.EXPECT().FindLatestByUID(ctx, uid).Return(&entity.KYC{
UID: uid,
Status: kyc.StatusREJECTED,
}, nil)
mockKYCRepo.EXPECT().Create(ctx, gomock.Any()).Return(nil)
},
expectedError: nil,
},
{
name: "已存在未駁回資料,禁止建立",
input: &entity.KYC{
UID: uid,
},
mockSetup: func() {
mockKYCRepo.EXPECT().FindLatestByUID(ctx, uid).Return(&entity.KYC{
UID: uid,
Status: kyc.StatusPending,
}, nil)
},
expectedError: errs.ForbiddenL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "param", Value: &entity.KYC{UID: uid}},
{Key: "func", Value: "KYCRepo.FindLatestByUID"},
{Key: "reason", Value: "KYC already in progress or approved"},
},
"不能重複送出 KYC 資料",
),
},
{
name: "查詢資料庫錯誤",
input: &entity.KYC{
UID: uid,
},
mockSetup: func() {
mockKYCRepo.EXPECT().FindLatestByUID(ctx, uid).Return(nil, errors.New("database error"))
},
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "param", Value: &entity.KYC{UID: uid}},
{Key: "func", Value: "KYCRepo.FindLatestByUID"},
{Key: "err", Value: "database error"},
},
"failed to get latest kyc",
),
},
{
name: "建立時資料庫錯誤",
input: &entity.KYC{
UID: uid,
},
mockSetup: func() {
mockKYCRepo.EXPECT().FindLatestByUID(ctx, uid).Return(nil, repo.ErrNotFound)
mockKYCRepo.EXPECT().Create(ctx, gomock.Any()).Return(errors.New("insert failed"))
},
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "param", Value: &entity.KYC{UID: uid, Status: kyc.StatusPending}},
{Key: "func", Value: "KYCRepo.Create"},
{Key: "err", Value: "insert failed"},
},
"failed to create kyc review",
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
err := useCase.Create(ctx, tt.input)
if tt.expectedError == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
}
})
}
}
func TestKYCUseCase_FindLatestByUID(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockKYCRepo := mockRepository.NewMockKYCRepository(mockCtrl)
useCase := MustKYCUseCase(KYCUseCaseParam{
KYCRepo: mockKYCRepo,
})
ctx := context.Background()
uid := "test-user"
tests := []struct {
name string
inputUID string
mockSetup func()
expectedResult *entity.KYC
expectedError error
}{
{
name: "查詢成功",
inputUID: uid,
mockSetup: func() {
mockKYCRepo.EXPECT().FindLatestByUID(ctx, uid).Return(&entity.KYC{
UID: uid,
Status: kyc.StatusPending,
}, nil)
},
expectedResult: &entity.KYC{
UID: uid,
Status: kyc.StatusPending,
},
expectedError: nil,
},
{
name: "資料庫錯誤",
inputUID: uid,
mockSetup: func() {
mockKYCRepo.EXPECT().FindLatestByUID(ctx, uid).Return(nil, errors.New("database error"))
},
expectedResult: nil,
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "uid", Value: uid},
{Key: "func", Value: "KYCRepo.FindLatestByUID"},
{Key: "err", Value: "database error"},
},
"failed to get latest kyc",
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
result, err := useCase.FindLatestByUID(ctx, tt.inputUID)
if tt.expectedError == nil {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, result)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
assert.Nil(t, result)
}
})
}
}
func TestKYCUseCase_FindByID(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockKYCRepo := mockRepository.NewMockKYCRepository(mockCtrl)
useCase := MustKYCUseCase(KYCUseCaseParam{
KYCRepo: mockKYCRepo,
})
ctx := context.Background()
objID := primitive.NewObjectID()
idStr := objID.Hex()
tests := []struct {
name string
inputID string
mockSetup func()
expectedResult *entity.KYC
expectedError error
}{
{
name: "查詢成功",
inputID: idStr,
mockSetup: func() {
mockKYCRepo.EXPECT().FindByID(ctx, idStr).Return(&entity.KYC{
ID: objID,
UID: "user-1",
Status: kyc.StatusAPPROVED,
}, nil)
},
expectedResult: &entity.KYC{
ID: objID,
UID: "user-1",
Status: kyc.StatusAPPROVED,
},
expectedError: nil,
},
{
name: "資料庫錯誤",
inputID: idStr,
mockSetup: func() {
mockKYCRepo.EXPECT().FindByID(ctx, idStr).Return(nil, errors.New("database error"))
},
expectedResult: nil,
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "id", Value: idStr},
{Key: "func", Value: "KYCRepo.FindByID"},
{Key: "err", Value: "database error"},
},
"failed to get kyc",
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
result, err := useCase.FindByID(ctx, tt.inputID)
if tt.expectedError == nil {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, result)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
assert.Nil(t, result)
}
})
}
}
func TestKYCUseCase_List(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockKYCRepo := mockRepository.NewMockKYCRepository(mockCtrl)
useCase := MustKYCUseCase(KYCUseCaseParam{
KYCRepo: mockKYCRepo,
})
ctx := context.Background()
uid := "user-1"
country := "TW"
status := kyc.StatusAPPROVED
ss := status.ToString()
tests := []struct {
name string
inputParams usecase.KYCQueryParams
mockSetup func()
expectedList []*entity.KYC
expectedCount int64
expectedError error
}{
{
name: "查詢成功",
inputParams: usecase.KYCQueryParams{
PageIndex: 1,
PageSize: 10,
UID: &uid,
Country: &country,
Status: &ss,
},
mockSetup: func() {
mockKYCRepo.EXPECT().List(ctx, repository.KYCQueryParams{
PageIndex: 1,
PageSize: 10,
UID: &uid,
Country: &country,
Status: &ss,
SortByDate: true,
}).Return([]*entity.KYC{
{UID: uid, CountryRegion: country, Status: status},
}, int64(1), nil)
},
expectedList: []*entity.KYC{
{UID: uid, CountryRegion: country, Status: status},
},
expectedCount: 1,
expectedError: nil,
},
{
name: "查詢失敗 - 資料庫錯誤",
inputParams: usecase.KYCQueryParams{
PageIndex: 2,
PageSize: 20,
},
mockSetup: func() {
mockKYCRepo.EXPECT().List(ctx, repository.KYCQueryParams{
PageIndex: 2,
PageSize: 20,
SortByDate: true,
}).Return(nil, int64(0), errors.New("database error"))
},
expectedList: nil,
expectedCount: 0,
expectedError: errs.DBErrorL(logx.WithContext(ctx),
[]logx.LogField{
{Key: "params", Value: usecase.KYCQueryParams{
PageIndex: 2,
PageSize: 20,
}},
{Key: "func", Value: "KYCRepo.List"},
{Key: "err", Value: "database error"},
}, "failed to list kyc"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
list, count, err := useCase.List(ctx, tt.inputParams)
assert.Equal(t, tt.expectedList, list)
assert.Equal(t, tt.expectedCount, count)
if tt.expectedError == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
}
})
}
}
func TestKYCUseCase_UpdateStatus(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockKYCRepo := mockRepository.NewMockKYCRepository(mockCtrl)
useCase := MustKYCUseCase(KYCUseCaseParam{
KYCRepo: mockKYCRepo,
})
ctx := context.Background()
id := primitive.NewObjectID().Hex()
status := kyc.StatusAPPROVED
reason := "all good"
tests := []struct {
name string
id string
status kyc.Status
reason string
mockSetup func()
expectedError error
}{
{
name: "更新成功",
id: id,
status: status,
reason: reason,
mockSetup: func() {
mockKYCRepo.EXPECT().UpdateStatus(ctx, id, status.ToString(), reason).Return(nil)
},
expectedError: nil,
},
{
name: "更新失敗 - DB 錯誤",
id: id,
status: status,
reason: reason,
mockSetup: func() {
mockKYCRepo.EXPECT().UpdateStatus(ctx, id, status.ToString(), reason).Return(errors.New("db error"))
},
expectedError: errs.DBErrorL(
logx.WithContext(ctx),
[]logx.LogField{
{Key: "params", Value: fmt.Sprintf("id:%s, status:%s, reason: %s", id, status, reason)},
{Key: "func", Value: "KYCRepo.UpdateStatus"},
{Key: "err", Value: "db error"},
},
"failed to update kyc status",
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
err := useCase.UpdateStatus(ctx, tt.id, tt.status.ToString(), tt.reason)
if tt.expectedError == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
}
})
}
}
func TestKYCUseCase_UpdateKYCInfo(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockKYCRepo := mockRepository.NewMockKYCRepository(mockCtrl)
useCase := MustKYCUseCase(KYCUseCaseParam{
KYCRepo: mockKYCRepo,
})
ctx := context.Background()
id := primitive.NewObjectID().Hex()
updateParams := &usecase.KYCUpdateParams{
Name: proto.String("Daniel Wang"),
Identification: proto.String("A123456789"),
IdentificationType: proto.String("passport"),
Address: proto.String("Taipei City"),
PostalCode: proto.String("100"),
DateOfBirth: proto.String("1993-04-17"),
Gender: proto.String("M"),
IDFrontImage: proto.String("https://example.com/front.jpg"),
IDBackImage: proto.String("https://example.com/back.jpg"),
BankStatementImg: proto.String("https://example.com/bank.jpg"),
BankCode: proto.String("123"),
BankName: proto.String("ABC Bank"),
BranchCode: proto.String("001"),
BranchName: proto.String("Taipei Branch"),
BankAccount: proto.String("1234567890"),
}
tests := []struct {
name string
id string
input *usecase.KYCUpdateParams
mockSetup func()
expectedError error
}{
{
name: "更新成功",
id: id,
input: updateParams,
mockSetup: func() {
mockKYCRepo.EXPECT().UpdateKYCInfo(ctx, id, &repository.KYCUpdateParams{
Name: updateParams.Name,
Identification: updateParams.Identification,
IdentificationType: updateParams.IdentificationType,
Address: updateParams.Address,
PostalCode: updateParams.PostalCode,
DateOfBirth: updateParams.DateOfBirth,
Gender: updateParams.Gender,
IDFrontImage: updateParams.IDFrontImage,
IDBackImage: updateParams.IDBackImage,
BankStatementImg: updateParams.BankStatementImg,
BankCode: updateParams.BankCode,
BankName: updateParams.BankName,
BranchCode: updateParams.BranchCode,
BranchName: updateParams.BranchName,
BankAccount: updateParams.BankAccount,
}).Return(nil)
},
expectedError: nil,
},
{
name: "更新失敗 - DB 錯誤",
id: id,
input: updateParams,
mockSetup: func() {
mockKYCRepo.EXPECT().UpdateKYCInfo(ctx, id, gomock.Any()).Return(errors.New("db error"))
},
expectedError: errs.DBErrorL(logx.WithContext(ctx),
[]logx.LogField{
{Key: "id", Value: id},
{Key: "params", Value: updateParams},
{Key: "func", Value: "KYCRepo.UpdateKYCInfo"},
{Key: "err", Value: "db error"},
},
"failed to update kyc",
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockSetup()
err := useCase.UpdateKYCInfo(ctx, tt.id, tt.input)
if tt.expectedError == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Equal(t, tt.expectedError.Error(), err.Error())
}
})
}
}

640
pkg/usecase/product_test.go Normal file
View File

@ -0,0 +1,640 @@
package usecase
import (
"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"
"context"
"errors"
"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"
"testing"
"time"
)
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
}