package hostorg import ( "encoding/json" "strings" "testing" "hyapp-admin-server/internal/integration/userclient" ) // TestNormalizeListQueryAllowsComputedSorts 锁定后台列表支持的计算排序字段不会在查询归一化时被清掉。 func TestNormalizeListQueryAllowsComputedSorts(t *testing.T) { for _, sortBy := range []string{sortByMerchantBalance, sortBySalaryWallet, sortByTotalRechargeUSDT} { query := normalizeListQuery(listQuery{Page: 1, PageSize: 50, SortBy: sortBy, SortDirection: "asc"}) if query.SortBy != sortBy || query.SortDirection != "asc" { t.Fatalf("expected sort %s asc to be kept, got sort_by=%q direction=%q", sortBy, query.SortBy, query.SortDirection) } } } // TestManagerListItemJSONHasNoAgencyOrParentBD 锁定后台经理列表的响应口径:经理是 BD Leader/Admin 的上级,不暴露 Agency 或上级 BD 字段。 func TestManagerListItemJSONHasNoAgencyOrParentBD(t *testing.T) { item := ManagerListItem{ UserID: 316033326332776448, DisplayUserID: "165549", Username: "Kitty", Status: "active", Contact: "+63", BDLeaderCount: 3, LastInvitedAtMs: 1720000000000, } payload, err := json.Marshal(item) if err != nil { t.Fatalf("marshal manager list item: %v", err) } body := string(payload) for _, want := range []string{ `"userId":"316033326332776448"`, `"displayUserId":"165549"`, `"contact":"+63"`, `"bdLeaderCount":3`, `"lastInvitedAtMs":1720000000000`, } { if !strings.Contains(body, want) { t.Fatalf("manager json should contain %s, got %s", want, body) } } if strings.Contains(body, "parentBd") || strings.Contains(body, "ParentBD") || strings.Contains(body, "agency") || strings.Contains(body, "Agency") { t.Fatalf("manager json must not expose agency or parent bd fields: %s", body) } } // TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix 锁定线上真实 FullPath 带 /api/v1 前缀时,BD Leader 状态仍然写独立负责人表。 func TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix(t *testing.T) { role, target := bdRoleFromStatusPath("/api/v1/admin/bd-leaders/:user_id/status") if role != "bd_leader" || target != "bd_leader_profiles" { t.Fatalf("leader status route must use bd_leader_profiles, got role=%q target=%q", role, target) } role, target = bdRoleFromStatusPath("/api/v1/admin/bds/:user_id/status") if role != "bd" || target != "bd_profiles" { t.Fatalf("bd status route must use bd_profiles, got role=%q target=%q", role, target) } } // TestUserCountryRegionSQLUsesCountryMapping 锁定组织列表区域展示口径:从用户国家映射 active 区域,不从身份表区域快照取值。 func TestUserCountryRegionSQLUsesCountryMapping(t *testing.T) { joinSQL := userCountryRegionJoin("owner", "owner_country_region", "owner_region") for _, want := range []string{ "LEFT JOIN region_countries owner_country_region", "owner_country_region.country_code = owner.country", "owner_country_region.status = 'active'", "LEFT JOIN regions owner_region", "owner_region.region_id = owner_country_region.region_id", "owner_region.status = 'active'", } { if !strings.Contains(joinSQL, want) { t.Fatalf("country region join missing %q in %s", want, joinSQL) } } if strings.Contains(joinSQL, "owner.region_id") { t.Fatalf("country region join must not read identity or user region snapshots: %s", joinSQL) } if got := userCountryRegionIDSQL("owner_region"); got != "COALESCE(owner_region.region_id, 0)" { t.Fatalf("region id projection mismatch: %s", got) } if got := userCountryRegionNameSQL("owner_region"); got != "COALESCE(owner_region.name, '')" { t.Fatalf("region name projection mismatch: %s", got) } } // TestSalaryWalletAssetTypeByRole 锁定四个组织身份读取各自工资钱包资产;BD Leader 复用 Admin 工资资产,不能误读普通 BD 资产。 func TestSalaryWalletAssetTypeByRole(t *testing.T) { cases := map[string]string{ "host": assetHostSalaryUSD, "agency": assetAgencySalaryUSD, "bd": assetBDSalaryUSD, "bd_leader": assetAdminSalaryUSD, "admin": assetAdminSalaryUSD, } for role, want := range cases { if got := salaryWalletAssetType(role); got != want { t.Fatalf("role %s salary wallet asset mismatch: got %q want %q", role, got, want) } } } // TestNormalizeSalaryWalletHistoryQueryCapsPageSize 避免历史抽屉一次拉太多钱包分录,同时兼容 admin 作为 BD Leader 的旧口径。 func TestNormalizeSalaryWalletHistoryQueryCapsPageSize(t *testing.T) { query := normalizeSalaryWalletHistoryQuery(salaryWalletHistoryQuery{Page: 0, PageSize: 500, Role: "admin", UserID: 7}) if query.Page != 1 || query.PageSize != 100 || query.Role != salaryWalletRoleBDLeader || query.UserID != 7 { t.Fatalf("salary wallet history query normalized incorrectly: %+v", query) } } // TestSortCoinSellerListItemsComputedFields 验证 wallet 聚合字段排序只改变展示顺序,不改动币商身份数据。 func TestSortCoinSellerListItemsComputedFields(t *testing.T) { items := []*CoinSellerListItem{ {UserID: 1, MerchantBalance: 100, TotalRechargeUSDTMicro: 30_000_000}, {UserID: 2, MerchantBalance: 300, TotalRechargeUSDTMicro: 10_000_000}, {UserID: 3, MerchantBalance: 200, TotalRechargeUSDTMicro: 20_000_000}, } sortCoinSellerListItems(items, sortByMerchantBalance, "desc") if got := []int64{items[0].UserID, items[1].UserID, items[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 { t.Fatalf("merchant balance desc order mismatch: %v", got) } sortCoinSellerListItems(items, sortByTotalRechargeUSDT, "asc") if got := []int64{items[0].UserID, items[1].UserID, items[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 { t.Fatalf("total recharge usdt asc order mismatch: %v", got) } } // TestSortSalaryWalletComputedFields 覆盖四个组织列表的工资钱包余额排序;这些余额来自 wallet-service,必须在分页前排序。 func TestSortSalaryWalletComputedFields(t *testing.T) { bds := []*userclient.BDProfile{ {UserID: 1, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 100}}, {UserID: 2, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 300}}, {UserID: 3, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 200}}, } sortBDProfilesBySalaryWallet(bds, "desc") if got := []int64{bds[0].UserID, bds[1].UserID, bds[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 { t.Fatalf("bd salary wallet desc order mismatch: %v", got) } if page := paginateBDProfiles(bds, 2, 2); len(page) != 1 || page[0].UserID != 1 { t.Fatalf("bd salary wallet pagination mismatch: %+v", page) } agencies := []*userclient.Agency{ {AgencyID: 10, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 100}}, {AgencyID: 20, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 300}}, {AgencyID: 30, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 200}}, } sortAgenciesBySalaryWallet(agencies, "asc") if got := []int64{agencies[0].AgencyID, agencies[1].AgencyID, agencies[2].AgencyID}; got[0] != 10 || got[1] != 30 || got[2] != 20 { t.Fatalf("agency salary wallet asc order mismatch: %v", got) } if page := paginateAgencies(agencies, 2, 2); len(page) != 1 || page[0].AgencyID != 20 { t.Fatalf("agency salary wallet pagination mismatch: %+v", page) } hosts := []*userclient.HostProfile{ {UserID: 1, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 100}}, {UserID: 2, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 300}}, {UserID: 3, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 200}}, } sortHostProfilesBySalaryWallet(hosts, "desc") if got := []int64{hosts[0].UserID, hosts[1].UserID, hosts[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 { t.Fatalf("host salary wallet desc order mismatch: %v", got) } if page := paginateHostProfiles(hosts, 2, 2); len(page) != 1 || page[0].UserID != 1 { t.Fatalf("host salary wallet pagination mismatch: %+v", page) } }