package usecase import ( "context" "errors" "fmt" "testing" "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" "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" ) 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()) } }) } }