320 lines
15 KiB
Go
320 lines
15 KiB
Go
package coinledger
|
|
|
|
import (
|
|
"context"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"hyapp-admin-server/internal/integration/walletclient"
|
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
)
|
|
|
|
type fakeCoinAdjustmentWallet struct {
|
|
walletclient.Client
|
|
req *walletv1.AdminCreditAssetRequest
|
|
}
|
|
|
|
func (f *fakeCoinAdjustmentWallet) AdminCreditAsset(_ context.Context, req *walletv1.AdminCreditAssetRequest) (*walletv1.AdminCreditAssetResponse, error) {
|
|
f.req = req
|
|
return &walletv1.AdminCreditAssetResponse{
|
|
TransactionId: "tx-admin-coin-adjustment",
|
|
Balance: &walletv1.AssetBalance{AvailableAmount: 880},
|
|
}, nil
|
|
}
|
|
|
|
func TestCreateCoinAdjustmentRequiresRemark(t *testing.T) {
|
|
service := NewService(nil, nil, nil, &fakeCoinAdjustmentWallet{})
|
|
_, err := service.CreateCoinAdjustment(context.Background(), "lalu", 90001, "req-coin-remark", coinAdjustmentRequest{
|
|
TargetUserID: "20001",
|
|
Amount: 100,
|
|
Reason: "manual adjustment",
|
|
Remark: " ",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "remark is required") {
|
|
t.Fatalf("expected required remark error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCreateCoinAdjustmentRejectsLongRemark(t *testing.T) {
|
|
service := NewService(nil, nil, nil, &fakeCoinAdjustmentWallet{})
|
|
_, err := service.CreateCoinAdjustment(context.Background(), "lalu", 90001, "req-coin-remark", coinAdjustmentRequest{
|
|
TargetUserID: "20001",
|
|
Amount: 100,
|
|
Reason: "manual adjustment",
|
|
Remark: strings.Repeat("r", 513),
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "remark is too long") {
|
|
t.Fatalf("expected long remark error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCreateCoinAdjustmentRemarkLengthUsesCharacters(t *testing.T) {
|
|
service := NewService(nil, nil, nil, &fakeCoinAdjustmentWallet{})
|
|
_, validErr := service.CreateCoinAdjustment(context.Background(), "lalu", 90001, "req-coin-remark", coinAdjustmentRequest{
|
|
TargetUserID: "20001",
|
|
Amount: 100,
|
|
Reason: "manual adjustment",
|
|
Remark: strings.Repeat("补", 512),
|
|
})
|
|
if validErr == nil || strings.Contains(validErr.Error(), "remark is too long") {
|
|
t.Fatalf("512 multibyte characters should pass length validation, got %v", validErr)
|
|
}
|
|
|
|
_, longErr := service.CreateCoinAdjustment(context.Background(), "lalu", 90001, "req-coin-remark", coinAdjustmentRequest{
|
|
TargetUserID: "20001",
|
|
Amount: 100,
|
|
Reason: "manual adjustment",
|
|
Remark: strings.Repeat("补", 513),
|
|
})
|
|
if longErr == nil || !strings.Contains(longErr.Error(), "remark is too long") {
|
|
t.Fatalf("expected long multibyte remark error, got %v", longErr)
|
|
}
|
|
}
|
|
|
|
func TestCreateCoinAdjustmentTrimsAndSendsRemark(t *testing.T) {
|
|
userDB, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("new sqlmock failed: %v", err)
|
|
}
|
|
defer userDB.Close()
|
|
|
|
mock.ExpectQuery(regexp.QuoteMeta("SELECT u.user_id, u.current_display_user_id, COALESCE(u.username, ''), COALESCE(u.avatar, '')")).
|
|
WithArgs("lalu", "20001", "20001", "20001", sqlmock.AnyArg(), "20001", "%20001%", "20001", "20001", "20001", sqlmock.AnyArg(), "20001").
|
|
WillReturnRows(sqlmock.NewRows([]string{"user_id", "current_display_user_id", "username", "avatar"}).AddRow(int64(20001), "6688", "target", "avatar.png"))
|
|
|
|
wallet := &fakeCoinAdjustmentWallet{}
|
|
service := NewService(userDB, nil, nil, wallet)
|
|
result, err := service.CreateCoinAdjustment(context.Background(), "lalu", 90001, "req-coin-remark", coinAdjustmentRequest{
|
|
CommandID: " cmd-admin-coin ",
|
|
TargetUserID: "20001",
|
|
Amount: 120,
|
|
Reason: " manual adjustment ",
|
|
Remark: " ticket approved by finance ",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateCoinAdjustment failed: %v", err)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("sql expectations mismatch: %v", err)
|
|
}
|
|
if wallet.req == nil {
|
|
t.Fatalf("wallet request was not sent")
|
|
}
|
|
if wallet.req.GetRemark() != "ticket approved by finance" || wallet.req.GetReason() != "manual adjustment" {
|
|
t.Fatalf("wallet audit fields mismatch: reason=%q remark=%q", wallet.req.GetReason(), wallet.req.GetRemark())
|
|
}
|
|
if wallet.req.GetCommandId() != "cmd-admin-coin" || wallet.req.GetTargetUserId() != 20001 || wallet.req.GetAmount() != 120 || wallet.req.GetOperatorUserId() != 90001 {
|
|
t.Fatalf("wallet request mismatch: %+v", wallet.req)
|
|
}
|
|
if result.Remark != "ticket approved by finance" || result.Reason != "manual adjustment" || result.BalanceAfter != 880 {
|
|
t.Fatalf("create dto mismatch: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeListQueryCapsPageSize(t *testing.T) {
|
|
query := normalizeListQuery(listQuery{Page: -1, PageSize: 1000, UserKeyword: " 10001 "})
|
|
if query.Page != 1 || query.PageSize != 100 || query.UserKeyword != "10001" {
|
|
t.Fatalf("normalized query mismatch: %+v", query)
|
|
}
|
|
}
|
|
|
|
func TestCoinLedgerWhereUsesTimeWindowAndUserFilter(t *testing.T) {
|
|
query := listQuery{StartAtMS: 100, EndAtMS: 200}
|
|
where, args := coinLedgerWhere("lalu", query, []int64{11, 22})
|
|
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND e.user_id IN (?,?)"; where != want {
|
|
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
|
}
|
|
if len(args) != 6 || args[0] != "lalu" || args[1] != coinAssetType || args[2] != int64(100) || args[3] != int64(200) || args[4] != int64(11) || args[5] != int64(22) {
|
|
t.Fatalf("args mismatch: %#v", args)
|
|
}
|
|
}
|
|
|
|
func TestCoinLedgerWhereUsesBizTypeFilter(t *testing.T) {
|
|
query := listQuery{BizType: "game_debit", StartAtMS: 100, EndAtMS: 200}
|
|
where, args := coinLedgerWhere("lalu", query, []int64{11})
|
|
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND e.user_id IN (?)"; where != want {
|
|
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
|
}
|
|
if len(args) != 6 || args[0] != "lalu" || args[1] != coinAssetType || args[2] != "game_debit" || args[3] != int64(100) || args[4] != int64(200) || args[5] != int64(11) {
|
|
t.Fatalf("args mismatch: %#v", args)
|
|
}
|
|
}
|
|
|
|
func TestCoinAdjustmentWhereLimitsManualCreditCoinEntries(t *testing.T) {
|
|
query := listQuery{StartAtMS: 100, EndAtMS: 200}
|
|
where, args := coinAdjustmentWhere("lalu", query, []int64{11})
|
|
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND e.user_id IN (?)"; where != want {
|
|
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
|
}
|
|
if len(args) != 6 || args[0] != "lalu" || args[1] != coinAssetType || args[2] != coinManualCreditBizType || args[3] != int64(100) || args[4] != int64(200) || args[5] != int64(11) {
|
|
t.Fatalf("args mismatch: %#v", args)
|
|
}
|
|
}
|
|
|
|
func TestCoinSellerLedgerWhereUsesExactSellerAndType(t *testing.T) {
|
|
query := coinSellerLedgerQuery{SellerUserID: 3001, LedgerType: coinSellerLedgerTypeSellerTransfer, StartAtMS: 100, EndAtMS: 200}
|
|
where, args, err := coinSellerLedgerWhere("lalu", query, []int64{3001})
|
|
if err != nil {
|
|
t.Fatalf("coin seller ledger where failed: %v", err)
|
|
}
|
|
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND e.user_id IN (?)"; where != want {
|
|
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
|
}
|
|
if len(args) != 6 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerTransferBizType || args[3] != int64(100) || args[4] != int64(200) || args[5] != int64(3001) {
|
|
t.Fatalf("args mismatch: %#v", args)
|
|
}
|
|
}
|
|
|
|
func TestCoinSellerLedgerWhereUsesSubSellerTransferType(t *testing.T) {
|
|
query := coinSellerLedgerQuery{SellerUserID: 3001, LedgerType: coinSellerLedgerTypeSubSellerTransfer, StartAtMS: 100, EndAtMS: 200}
|
|
where, args, err := coinSellerLedgerWhere("lalu", query, []int64{3001})
|
|
if err != nil {
|
|
t.Fatalf("coin seller ledger where failed: %v", err)
|
|
}
|
|
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND e.user_id IN (?)"; where != want {
|
|
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
|
}
|
|
if len(args) != 6 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerSubTransferBizType || args[3] != int64(100) || args[4] != int64(200) || args[5] != int64(3001) {
|
|
t.Fatalf("args mismatch: %#v", args)
|
|
}
|
|
}
|
|
|
|
func TestCoinSellerLedgerWhereMapsAdminStockCredit(t *testing.T) {
|
|
query := coinSellerLedgerQuery{LedgerType: coinSellerLedgerTypeAdminStockCredit}
|
|
where, args, err := coinSellerLedgerWhere("lalu", query, []int64{3001, 3002})
|
|
if err != nil {
|
|
t.Fatalf("coin seller ledger where failed: %v", err)
|
|
}
|
|
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?,?) AND e.user_id IN (?,?)"; where != want {
|
|
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
|
}
|
|
if len(args) != 9 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerStockDeductionBizType || args[4] != coinSellerRechargeBizType || args[5] != coinSellerCoinCompensationBizType || args[6] != coinManualCreditBizType || args[7] != int64(3001) || args[8] != int64(3002) {
|
|
t.Fatalf("args mismatch: %#v", args)
|
|
}
|
|
}
|
|
|
|
func TestCoinSellerLedgerWhereEmptyTypeUsesAllPublicTypes(t *testing.T) {
|
|
where, args, err := coinSellerLedgerWhere("lalu", coinSellerLedgerQuery{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("coin seller ledger where failed: %v", err)
|
|
}
|
|
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?,?,?,?,?)"; where != want {
|
|
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
|
}
|
|
if len(args) != 10 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerStockDeductionBizType || args[4] != coinSellerRechargeBizType || args[5] != coinSellerCoinCompensationBizType || args[6] != coinSellerTransferBizType || args[7] != coinSellerSubTransferBizType || args[8] != salaryTransferToCoinSellerBizType || args[9] != coinManualCreditBizType {
|
|
t.Fatalf("args mismatch: %#v", args)
|
|
}
|
|
}
|
|
|
|
func TestCoinSellerLedgerWhereRejectsInvalidType(t *testing.T) {
|
|
_, _, err := coinSellerLedgerWhere("lalu", coinSellerLedgerQuery{LedgerType: "bad_type"}, nil)
|
|
if err == nil {
|
|
t.Fatalf("expected invalid ledger type error")
|
|
}
|
|
}
|
|
|
|
func TestCoinSellerLedgerReceiverUserIDUsesActualReceiver(t *testing.T) {
|
|
metadata := map[string]any{"target_user_id": float64(4001)}
|
|
if got := coinSellerLedgerReceiverUserID(coinSellerTransferBizType, 3001, 0, metadata); got != 4001 {
|
|
t.Fatalf("seller transfer receiver mismatch: %d", got)
|
|
}
|
|
subTransferMetadata := map[string]any{"child_user_id": float64(5001)}
|
|
if got := coinSellerLedgerReceiverUserID(coinSellerSubTransferBizType, 3001, 4001, subTransferMetadata); got != 5001 {
|
|
t.Fatalf("sub seller transfer receiver mismatch: %d", got)
|
|
}
|
|
if got := coinSellerLedgerReceiverUserID(coinSellerSubTransferBizType, 5001, 3001, subTransferMetadata); got != 5001 {
|
|
t.Fatalf("sub seller income receiver mismatch: %d", got)
|
|
}
|
|
if got := coinSellerLedgerReceiverUserID(coinSellerStockPurchaseBizType, 3001, 4001, metadata); got != 3001 {
|
|
t.Fatalf("admin stock receiver mismatch: %d", got)
|
|
}
|
|
if got := coinSellerLedgerReceiverUserID(salaryTransferToCoinSellerBizType, 3001, 4001, metadata); got != 3001 {
|
|
t.Fatalf("salary transfer receiver mismatch: %d", got)
|
|
}
|
|
}
|
|
|
|
func TestCoinSellerLedgerOperatorExportFieldsOnlyForAdminStock(t *testing.T) {
|
|
adminItem := coinSellerLedgerDTO{
|
|
LedgerType: coinSellerLedgerTypeAdminStockCredit,
|
|
OperatorUserID: "7",
|
|
Operator: coinAdjustmentOperatorDTO{AdminID: "7", Username: "hyappadmin", Name: "Admin"},
|
|
}
|
|
operatorName, operatorID := coinSellerLedgerOperatorExportFields(adminItem)
|
|
if operatorName != "hyappadmin" || operatorID != "7" {
|
|
t.Fatalf("operator fields mismatch: name=%q id=%q", operatorName, operatorID)
|
|
}
|
|
|
|
transferItem := coinSellerLedgerDTO{LedgerType: coinSellerLedgerTypeSellerTransfer, OperatorUserID: "7"}
|
|
operatorName, operatorID = coinSellerLedgerOperatorExportFields(transferItem)
|
|
if operatorName != "" || operatorID != "" {
|
|
t.Fatalf("seller transfer should not export operator: name=%q id=%q", operatorName, operatorID)
|
|
}
|
|
}
|
|
|
|
func TestCoinSellerLedgerLabelUsesConcreteStockAndDebitType(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
item coinSellerLedgerDTO
|
|
want string
|
|
}{
|
|
{name: "usdt purchase", item: coinSellerLedgerDTO{BizType: coinSellerStockPurchaseBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "USDT进货"},
|
|
{name: "usdt deduction", item: coinSellerLedgerDTO{BizType: coinSellerStockDeductionBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "USDT扣除"},
|
|
{name: "third party recharge", item: coinSellerLedgerDTO{BizType: coinSellerRechargeBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "三方充值"},
|
|
{name: "compensation", item: coinSellerLedgerDTO{BizType: coinSellerCoinCompensationBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "金币补偿"},
|
|
{name: "debit", item: coinSellerLedgerDTO{BizType: coinManualCreditBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit, AvailableDelta: -100}, want: "金币扣除"},
|
|
{name: "manual increase", item: coinSellerLedgerDTO{BizType: coinManualCreditBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit, AvailableDelta: 100}, want: "金币增加"},
|
|
{name: "sub seller transfer", item: coinSellerLedgerDTO{BizType: coinSellerSubTransferBizType, LedgerType: coinSellerLedgerTypeSubSellerTransfer}, want: "向子币商转账"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := coinSellerLedgerLabel(tc.item); got != tc.want {
|
|
t.Fatalf("%s label mismatch: got %q want %q", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCoinSellerLedgerCSVAmountFormatters(t *testing.T) {
|
|
if got := formatUSDMinorForCSV(50); got != "0.50" {
|
|
t.Fatalf("usd minor format mismatch: %q", got)
|
|
}
|
|
if got := formatUSDTMicroForCSV(1_230_000); got != "1.23" {
|
|
t.Fatalf("usdt micro format mismatch: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeCoinSellerLedgerQueryCapsPageSize(t *testing.T) {
|
|
query := normalizeCoinSellerLedgerQuery(coinSellerLedgerQuery{Page: -1, PageSize: 1000, SellerKeyword: " 164425 ", LedgerType: " seller_transfer "})
|
|
if query.Page != 1 || query.PageSize != 100 || query.SellerKeyword != "164425" || query.LedgerType != coinSellerLedgerTypeSellerTransfer {
|
|
t.Fatalf("normalized query mismatch: %+v", query)
|
|
}
|
|
}
|
|
|
|
func TestDirectionAndAmountForDelta(t *testing.T) {
|
|
if directionForDelta(-9) != directionOut || absInt64(-9) != 9 {
|
|
t.Fatalf("expense projection mismatch")
|
|
}
|
|
if directionForDelta(12) != directionIn || absInt64(12) != 12 {
|
|
t.Fatalf("income projection mismatch")
|
|
}
|
|
}
|
|
|
|
func TestParseMetadataJSON(t *testing.T) {
|
|
metadata, err := parseMetadataJSON(`{"gift_name":"Rose","gift_count":2,"target_user_id":1002}`)
|
|
if err != nil {
|
|
t.Fatalf("parse metadata failed: %v", err)
|
|
}
|
|
if metadata["gift_name"] != "Rose" || metadata["gift_count"] != float64(2) || metadata["target_user_id"] != float64(1002) {
|
|
t.Fatalf("metadata mismatch: %#v", metadata)
|
|
}
|
|
|
|
empty, err := parseMetadataJSON("null")
|
|
if err != nil {
|
|
t.Fatalf("parse empty metadata failed: %v", err)
|
|
}
|
|
if len(empty) != 0 {
|
|
t.Fatalf("empty metadata mismatch: %#v", empty)
|
|
}
|
|
}
|