2026-07-02 14:22:37 +08:00

142 lines
5.8 KiB
Go

package hostwithdrawal
import (
"os"
"reflect"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func TestNormalizeListQueryMapsIdentityAndRejectsBadInput(t *testing.T) {
query, err := normalizeListQuery(listQuery{
Page: -1,
PageSize: 1000,
AppCode: " LaLu ",
RecordType: "all",
Identity: "BD_LEADER",
Status: "all",
Keyword: " 168557 ",
StartAtMS: 100,
EndAtMS: 200,
})
if err != nil {
t.Fatalf("normalize query failed: %v", err)
}
if query.Page != 1 || query.PageSize != 100 || query.AppCode != "lalu" || query.RecordType != "" || query.Status != "" || query.Keyword != "168557" {
t.Fatalf("normalized basic fields mismatch: %+v", query)
}
if query.SalaryAssetType != assetAdminSalary || query.Identity != "bd_leader_admin" {
t.Fatalf("identity mapping mismatch: %+v", query)
}
if _, err := normalizeListQuery(listQuery{Status: "done"}); err == nil {
t.Fatal("expected invalid status to fail")
}
if _, err := normalizeListQuery(listQuery{StartAtMS: 200, EndAtMS: 200}); err == nil {
t.Fatal("expected closed time range to fail")
}
}
func TestSalaryTransferWhereUsesIdentityStatusTimeAndKeywordUsers(t *testing.T) {
where, args := salaryTransferWhere(listQuery{
AppCode: "lalu",
SalaryAssetType: assetHostSalary,
Keyword: "cmd",
StartAtMS: 100,
EndAtMS: 200,
}, []int64{11, 22})
wantWhere := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type = ? AND wt.status = ? AND JSON_UNQUOTE(JSON_EXTRACT(wt.metadata_json, '$.salary_asset_type')) = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND (e.user_id IN (?,?) OR e.counterparty_user_id IN (?,?) OR e.transaction_id LIKE ? OR wt.command_id LIKE ? OR CAST(e.user_id AS CHAR) LIKE ? OR CAST(e.counterparty_user_id AS CHAR) LIKE ?)"
if where != wantWhere {
t.Fatalf("where mismatch:\nwant %s\n got %s", wantWhere, where)
}
wantArgs := []any{"lalu", assetCoinSellerCoin, bizSalaryTransfer, transactionSucceeded, assetHostSalary, int64(100), int64(200), int64(11), int64(22), int64(11), int64(22), "%cmd%", "%cmd%", "%cmd%", "%cmd%"}
if !reflect.DeepEqual(args, wantArgs) {
t.Fatalf("args mismatch:\nwant %#v\n got %#v", wantArgs, args)
}
}
func TestWithdrawalWhereUsesStatusIdentityTimeAndKeywordUsers(t *testing.T) {
where, args := withdrawalWhere(listQuery{
AppCode: "lalu",
SalaryAssetType: assetBDSalary,
Status: statusPending,
Keyword: "upi",
StartAtMS: 100,
EndAtMS: 200,
}, []int64{77})
wantWhere := "WHERE a.app_code = ? AND a.salary_asset_type = ? AND a.status = ? AND a.created_at_ms >= ? AND a.created_at_ms < ? AND (a.user_id IN (?) OR CAST(a.id AS CHAR) LIKE ? OR a.user_id LIKE ? OR a.withdraw_method LIKE ? OR a.withdraw_address LIKE ? OR a.freeze_transaction_id LIKE ? OR a.audit_transaction_id LIKE ? OR a.approver_name LIKE ?)"
if where != wantWhere {
t.Fatalf("where mismatch:\nwant %s\n got %s", wantWhere, where)
}
wantArgs := []any{"lalu", assetBDSalary, statusPending, int64(100), int64(200), "77", "%upi%", "%upi%", "%upi%", "%upi%", "%upi%", "%upi%", "%upi%"}
if !reflect.DeepEqual(args, wantArgs) {
t.Fatalf("args mismatch:\nwant %#v\n got %#v", wantArgs, args)
}
}
func TestPageMergedItemsSortsAndSlicesStableMixedSources(t *testing.T) {
items := []itemDTO{
{ID: "salary_transfer:8", RecordType: recordTypeSalaryTransfer, CreatedAtMS: 100},
{ID: "withdrawal:7", RecordType: recordTypeWithdrawal, ApplicationID: "7", CreatedAtMS: 100},
{ID: "salary_transfer:9", RecordType: recordTypeSalaryTransfer, CreatedAtMS: 100},
{ID: "withdrawal:11", RecordType: recordTypeWithdrawal, ApplicationID: "11", CreatedAtMS: 90},
{ID: "salary_transfer:1", RecordType: recordTypeSalaryTransfer, CreatedAtMS: 80},
}
page := pageMergedItems(items, 1, 4)
gotIDs := []string{page[0].ID, page[1].ID, page[2].ID, page[3].ID}
wantIDs := []string{"salary_transfer:9", "salary_transfer:8", "withdrawal:7", "withdrawal:11"}
if !reflect.DeepEqual(gotIDs, wantIDs) {
t.Fatalf("page sort mismatch:\nwant %#v\n got %#v", wantIDs, gotIDs)
}
secondPage := pageMergedItems(items, 2, 2)
gotIDs = []string{secondPage[0].ID, secondPage[1].ID}
wantIDs = []string{"withdrawal:7", "withdrawal:11"}
if !reflect.DeepEqual(gotIDs, wantIDs) {
t.Fatalf("second page mismatch:\nwant %#v\n got %#v", wantIDs, gotIDs)
}
}
func TestRegisterRoutesExposesHostWithdrawalsEndpoint(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
group := engine.Group("/api/v1")
RegisterRoutes(group, &Handler{})
for _, route := range engine.Routes() {
if route.Method == "GET" && route.Path == "/api/v1/admin/host-withdrawals" {
return
}
}
t.Fatal("host withdrawals route was not registered")
}
func TestHostWithdrawalIndexSQLKeepsRequiredReadPaths(t *testing.T) {
adminSQL, err := os.ReadFile("../../../migrations/072_host_withdrawal_navigation.sql")
if err != nil {
t.Fatalf("read admin migration failed: %v", err)
}
walletSQL, err := os.ReadFile("../../../../../scripts/mysql/054_wallet_entries_asset_counterparty_time_index.sql")
if err != nil {
t.Fatalf("read wallet migration failed: %v", err)
}
for _, snippet := range []string{
"idx_admin_withdrawal_app_time (app_code, created_at_ms, id)",
"idx_admin_withdrawal_app_asset_time (app_code, salary_asset_type, created_at_ms, id)",
"idx_admin_withdrawal_app_status_asset_time (app_code, status, salary_asset_type, created_at_ms, id)",
"idx_admin_withdrawal_app_user_time (app_code, user_id, created_at_ms, id)",
} {
if !strings.Contains(string(adminSQL), snippet) {
t.Fatalf("admin migration missing index snippet %q", snippet)
}
}
if !strings.Contains(string(walletSQL), "idx_wallet_entries_asset_counterparty_time (app_code, asset_type, counterparty_user_id, created_at_ms, entry_id)") {
t.Fatal("wallet migration missing counterparty index")
}
}