2026-07-05 01:55:03 +08:00

255 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package hostorg
import (
"context"
"encoding/json"
"reflect"
"strconv"
"strings"
"testing"
"hyapp-admin-server/internal/integration/userclient"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type fakeHostOrgUserClient struct {
userclient.Client
users map[int64]*userclient.User
displays map[string]*userclient.UserIdentity
calls []string
}
func (f *fakeHostOrgUserClient) GetUser(_ context.Context, req userclient.GetUserRequest) (*userclient.User, error) {
f.calls = append(f.calls, "get:"+strconv.FormatInt(req.UserID, 10))
if user := f.users[req.UserID]; user != nil {
return user, nil
}
return nil, status.Error(codes.NotFound, "user not found")
}
func (f *fakeHostOrgUserClient) ResolveDisplayUserID(_ context.Context, req userclient.ResolveDisplayUserIDRequest) (*userclient.UserIdentity, error) {
f.calls = append(f.calls, "resolve:"+req.DisplayUserID)
if identity := f.displays[req.DisplayUserID]; identity != nil {
return identity, nil
}
return nil, status.Error(codes.NotFound, "display_user_id not found")
}
func TestResolveDisplayUserIDFallsBackToNumericUserID(t *testing.T) {
client := &fakeHostOrgUserClient{
users: map[int64]*userclient.User{
123456: {UserID: 123456, DisplayUserID: "9999", DefaultDisplayUserID: "123456"},
},
displays: map[string]*userclient.UserIdentity{},
}
service := NewService(client, nil, nil)
userID, err := service.resolveDisplayUserID(context.Background(), "req-held-default", 123456)
if err != nil {
t.Fatalf("resolveDisplayUserID returned error: %v", err)
}
if userID != 123456 {
t.Fatalf("fallback user id mismatch: got %d", userID)
}
if want := []string{"resolve:123456", "get:123456"}; !reflect.DeepEqual(client.calls, want) {
t.Fatalf("calls mismatch: got %v want %v", client.calls, want)
}
}
func TestResolveDisplayUserIDKeepsActiveDisplayPriority(t *testing.T) {
client := &fakeHostOrgUserClient{
users: map[int64]*userclient.User{
123456: {UserID: 123456, DisplayUserID: "9999", DefaultDisplayUserID: "123456"},
},
displays: map[string]*userclient.UserIdentity{
"123456": {UserID: 88, DisplayUserID: "123456"},
},
}
service := NewService(client, nil, nil)
userID, err := service.resolveDisplayUserID(context.Background(), "req-active-display", 123456)
if err != nil {
t.Fatalf("resolveDisplayUserID returned error: %v", err)
}
if userID != 88 {
t.Fatalf("active display user id mismatch: got %d", userID)
}
if want := []string{"resolve:123456"}; !reflect.DeepEqual(client.calls, want) {
t.Fatalf("calls mismatch: got %v want %v", client.calls, want)
}
}
// 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",
CanGrantVehicle: true,
CanGrantVIP: true,
CanBlockUser: false,
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"`,
`"canGrantVehicle":true`,
`"canGrantVip":true`,
`"canBlockUser":false`,
`"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)
}
}