package user import ( "context" "fmt" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" logmodel "github.com/perfect-panel/server/internal/model/log" usermodel "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/driver/mysql" "gorm.io/gorm" ) func TestQueryWithdrawalLog_WithWithdrawalBizType(t *testing.T) { const userID = int64(42) createdAt := time.Unix(1700000000, 0) updatedAt := createdAt.Add(time.Minute) db, mock, cleanup := newQueryWithdrawalLogTestDB(t) defer cleanup() mock.ExpectQuery("SELECT count(*) FROM `withdrawals` WHERE user_id = ?"). WithArgs(userID). WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) mock.ExpectQuery("SELECT * FROM `withdrawals` WHERE user_id = ? ORDER BY id DESC LIMIT ?"). WithArgs(userID, 10). WillReturnRows(sqlmock.NewRows([]string{ "id", "user_id", "amount", "content", "status", "reason", "method", "account", "qr_code_url", "created_at", "updated_at", }).AddRow( int64(1001), userID, int64(3000), "bank withdrawal", usermodel.WithdrawalStatusPending, "", uint8(3), "acct-001", "", createdAt, updatedAt, )) logic := newTestQueryWithdrawalLogLogic(t, db, userID) resp, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{ BizType: withdrawalLogBizTypeWithdrawal, Page: 1, Size: 10, }) if err != nil { t.Fatalf("QueryWithdrawalLog unexpected error: %v", err) } if resp.Total != 1 || len(resp.List) != 1 { t.Fatalf("QueryWithdrawalLog response = %+v, want one withdrawal", resp) } got := resp.List[0] if got.BizType != withdrawalLogBizTypeWithdrawal { t.Fatalf("BizType = %q, want %q", got.BizType, withdrawalLogBizTypeWithdrawal) } if got.Id != 1001 || got.UserId != userID || got.Amount != 3000 || got.CreatedAt != createdAt.UnixMilli() || got.UpdatedAt != updatedAt.UnixMilli() { t.Fatalf("withdrawal item = %+v", got) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("unmet sql expectations: %v", err) } } func TestQueryWithdrawalLog_WithCommissionRefundBizType(t *testing.T) { const userID = int64(42) content := `{"type":333,"amount":2500,"order_no":"ORDER-1","timestamp":1700000000123}` createdAt := time.Unix(1700000000, 0) db, mock, cleanup := newQueryWithdrawalLogTestDB(t) defer cleanup() mock.ExpectQuery("SELECT count(*) FROM `system_logs` WHERE `type` = ? AND object_id = ? AND `content` LIKE ?"). WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%"). WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND `content` LIKE ? ORDER BY id DESC LIMIT ?"). WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", 10). WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}). AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, content, createdAt)) logic := newTestQueryWithdrawalLogLogic(t, db, userID) resp, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{ BizType: withdrawalLogBizTypeCommissionRefund, Page: 1, Size: 10, }) if err != nil { t.Fatalf("QueryWithdrawalLog unexpected error: %v", err) } if resp.Total != 1 || len(resp.List) != 1 { t.Fatalf("QueryWithdrawalLog response = %+v, want one commission refund", resp) } got := resp.List[0] if got.BizType != withdrawalLogBizTypeCommissionRefund { t.Fatalf("BizType = %q, want %q", got.BizType, withdrawalLogBizTypeCommissionRefund) } if got.Id != 2001 || got.UserId != userID || got.Amount != 2500 || got.Content != content || got.CreatedAt != 1700000000123 || got.UpdatedAt != 1700000000123 { t.Fatalf("commission refund item = %+v", got) } if got.Status != 0 || got.Method != 0 || got.Account != "" || got.QrCodeUrl != "" { t.Fatalf("commission refund withdrawal-only fields = %+v, want zero values", got) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("unmet sql expectations: %v", err) } } func TestQueryWithdrawalLog_RejectsInvalidBizType(t *testing.T) { db, mock, cleanup := newQueryWithdrawalLogTestDB(t) defer cleanup() logic := newTestQueryWithdrawalLogLogic(t, db, 42) _, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{ BizType: "all", Page: 1, Size: 10, }) if !isQueryWithdrawalLogErrCode(err, xerr.InvalidParams) { t.Fatalf("QueryWithdrawalLog err = %v, want InvalidParams", err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("unmet sql expectations: %v", err) } } func newQueryWithdrawalLogTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) { t.Helper() sqlDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherFunc(func(expectedSQL, actualSQL string) error { if strings.Contains(actualSQL, expectedSQL) { return nil } return fmt.Errorf("actual sql %q does not contain %q", actualSQL, expectedSQL) }))) if err != nil { t.Fatalf("create sqlmock: %v", err) } db, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{}) if err != nil { _ = sqlDB.Close() t.Fatalf("open gorm db: %v", err) } return db, mock, func() { _ = sqlDB.Close() } } func newTestQueryWithdrawalLogLogic(t *testing.T, db *gorm.DB, userID int64) *QueryWithdrawalLogLogic { t.Helper() ctx := context.WithValue(context.Background(), constant.CtxKeyUser, &usermodel.User{Id: userID}) return &QueryWithdrawalLogLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: &svc.ServiceContext{ DB: db, }, } } func queryWithdrawalLogErrCodeOf(err error) uint32 { if err == nil { return 0 } type coder interface { GetErrCode() uint32 } cause := errors.Cause(err) if c, ok := cause.(coder); ok { return c.GetErrCode() } return 0 } func isQueryWithdrawalLogErrCode(err error, code uint32) bool { return queryWithdrawalLogErrCodeOf(err) == code }