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") } }