hyapp-server/server/admin/internal/modules/payment/money_region_source_test.go
2026-07-14 17:12:39 +08:00

231 lines
8.9 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 payment
import (
"context"
"database/sql"
"database/sql/driver"
"strings"
"testing"
"time"
"hyapp-admin-server/internal/config"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/repository"
"github.com/DATA-DOG/go-sqlmock"
"go.mongodb.org/mongo-driver/v2/bson"
)
func TestLegacyRegionDocumentsToMoneyDataFiltersYumiRegions(t *testing.T) {
objectID, err := bson.ObjectIDFromHex("69d88849bdbcc07ebc3f118c")
if err != nil {
t.Fatalf("build object id failed: %v", err)
}
cfg := config.MoneyRegionSourceConfig{
AppCode: "yumi",
AppName: "Yumi",
LogoURL: "https://media.example.com/yumi.png",
SysOrigin: "LIKEI",
}
apps, regions, countries, err := legacyRegionDocumentsToMoneyData(cfg, []legacyRegionDocument{
{ID: objectID, RegionCode: "OTHER", SysOrigin: "LIKEI", RegionName: "Default", CountryCodes: "", Del: false},
{ID: "1001", RegionCode: "MIDDLE_EAST", SysOrigin: "LIKEI", RegionName: "中东区", CountryCodes: "SA, AE", Del: false},
{ID: "1002", RegionCode: "DELETED", SysOrigin: "LIKEI", RegionName: "已删区域", CountryCodes: "IN", Del: true},
{ID: "1003", RegionCode: "OTHER", SysOrigin: "OTHER", RegionName: "其他系统", CountryCodes: "US", Del: false},
}, repository.MoneyAccess{All: true})
if err != nil {
t.Fatalf("legacy money data failed: %v", err)
}
if len(apps) != 1 || apps[0].AppCode != "yumi" || apps[0].AppName != "Yumi" || apps[0].LogoURL != cfg.LogoURL || apps[0].Platform != "legacy" {
t.Fatalf("apps mismatch: %+v", apps)
}
if len(regions) != 2 || regions[0].RegionID < legacySyntheticRegionIDBase || regions[0].RegionCode != "OTHER" || regions[1].RegionID != 1001 || regions[1].RegionCode != "MIDDLE_EAST" {
t.Fatalf("regions mismatch: %+v", regions)
}
if len(countries) != 2 || countries[0].CountryCode != "SA" || countries[1].CountryCode != "AE" || countries[0].RegionID != 1001 {
t.Fatalf("countries mismatch: %+v", countries)
}
}
func TestLoadMoneyMasterDataIncludesScopedMoneyRegionSource(t *testing.T) {
userDB, sqlMock, cleanup := newMoneyRegionSourceUserSQLMock(t)
defer cleanup()
expectEmptyMoneyApps(sqlMock, "yumi")
handler := New(&mockPaymentWallet{}, userDB, nil, nil, nil, WithMoneyRegionSources(&staticMoneyRegionSource{
apps: []moneyAppDTO{{AppCode: "yumi", AppName: "Yumi", Platform: "legacy", Status: "active"}},
regions: []moneyRegionDTO{
{AppCode: "yumi", RegionID: 1001, RegionCode: "MIDDLE_EAST", Name: "中东区", Status: "active"},
{AppCode: "yumi", RegionID: 1002, RegionCode: "INDIA", Name: "印度区", Status: "active"},
},
}))
_, regions, _, err := handler.loadMoneyMasterData(context.Background(), repository.MoneyAccess{
Scopes: []model.UserMoneyScope{{AppCode: "yumi", RegionID: 1002}},
})
if err != nil {
t.Fatalf("load money master data failed: %v", err)
}
if len(regions) != 1 || regions[0].RegionID != 1002 || regions[0].RegionCode != "INDIA" {
t.Fatalf("scoped regions mismatch: %+v", regions)
}
if err := sqlMock.ExpectationsWereMet(); err != nil {
t.Fatalf("sql expectations mismatch: %v", err)
}
}
func TestLegacyRegionDocumentsMapNonNumericRegionIDStably(t *testing.T) {
cfg := config.MoneyRegionSourceConfig{
AppCode: "yumi",
AppName: "Yumi",
SysOrigin: "LIKEI",
}
document := legacyRegionDocument{ID: "69d88849bdbcc07ebc3f118c", RegionCode: "OTHER", SysOrigin: "LIKEI"}
first, rawFirst, err := legacyMoneyRegionID(cfg, document)
if err != nil {
t.Fatalf("map legacy id failed: %v", err)
}
second, rawSecond, err := legacyMoneyRegionID(cfg, document)
if err != nil {
t.Fatalf("map legacy id second time failed: %v", err)
}
if first != second || rawFirst != rawSecond || first < legacySyntheticRegionIDBase {
t.Fatalf("legacy id mapping is not stable: first=%d/%s second=%d/%s", first, rawFirst, second, rawSecond)
}
}
func TestLegacyRegionDocumentsRejectEmptyRegionID(t *testing.T) {
_, _, _, err := legacyRegionDocumentsToMoneyData(config.MoneyRegionSourceConfig{
AppCode: "yumi",
AppName: "Yumi",
SysOrigin: "LIKEI",
}, []legacyRegionDocument{{ID: "", RegionCode: "MIDDLE_EAST", SysOrigin: "LIKEI"}}, repository.MoneyAccess{All: true})
if err == nil {
t.Fatalf("expected empty region id error")
}
}
func TestCountryBelongsToRegionUsesMoneyRegionSource(t *testing.T) {
handler := New(&mockPaymentWallet{}, nil, nil, nil, nil, WithMoneyRegionSources(&staticMoneyRegionSource{
regions: []moneyRegionDTO{{AppCode: "yumi", RegionID: 1002, RegionCode: "INDIA", Name: "印度区", Countries: []string{"IN", "PK"}}},
}))
ok, err := handler.countryBelongsToRegion(context.Background(), "yumi", 1002, "in")
if err != nil {
t.Fatalf("country belongs to legacy region failed: %v", err)
}
if !ok {
t.Fatalf("expected legacy source country match")
}
}
type staticMoneyRegionSource struct {
apps []moneyAppDTO
regions []moneyRegionDTO
countries []moneyCountryDTO
}
func (s *staticMoneyRegionSource) ListMoneyMasterData(_ context.Context, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error) {
regions := []moneyRegionDTO{}
for _, region := range s.regions {
if access.All || access.Allows(region.AppCode, region.RegionID) {
regions = append(regions, region)
}
}
return s.apps, regions, s.countries, nil
}
func (s *staticMoneyRegionSource) CountryBelongsToRegion(_ context.Context, appCode string, regionID int64, countryCode string) (bool, bool, error) {
for _, region := range s.regions {
if region.AppCode != appCode || region.RegionID != regionID {
continue
}
for _, item := range region.Countries {
if item == strings.ToUpper(strings.TrimSpace(countryCode)) {
return true, true, nil
}
}
return false, true, nil
}
return false, false, nil
}
func newMoneyRegionSourceUserSQLMock(t *testing.T) (*sql.DB, sqlmock.Sqlmock, func()) {
t.Helper()
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sql mock failed: %v", err)
}
return db, mock, func() {
_ = db.Close()
}
}
func expectEmptyMoneyApps(mock sqlmock.Sqlmock, appCodes ...string) {
query := mock.ExpectQuery(`(?s)SELECT app_id, app_code, app_name, logo_url, package_name, platform, status\s+FROM apps\s+WHERE status = 'active'`)
if len(appCodes) > 0 {
args := make([]driver.Value, 0, len(appCodes))
for _, appCode := range appCodes {
args = append(args, appCode)
}
query.WithArgs(args...)
}
query.WillReturnRows(sqlmock.NewRows([]string{"app_id", "app_code", "app_name", "logo_url", "package_name", "platform", "status"}))
}
func TestMoneyRegionSourceConfigDefaults(t *testing.T) {
cfg := config.Default()
if len(cfg.MoneyRegionSources) != 2 || cfg.MoneyRegionSources[0].RequestTimeout != 5*time.Second || cfg.MoneyRegionSources[0].LogoURL == "" || cfg.MoneyRegionSources[1].AppCode != "aslan" {
t.Fatalf("default money region source mismatch: %+v", cfg.MoneyRegionSources)
}
}
func TestRegionIDForCountryCodeInRegionsFirstMatchWins(t *testing.T) {
// 目录顺序即 createTime 倒序MM 同时挂在缅甸区和巴基斯坦区时必须命中最新的(与账单
// legacyCountryRegionIndex 同优先级),否则同一用户的账单和币商订单会归进两个区域。
regions := []moneyRegionDTO{
{AppCode: "aslan", RegionID: 2064044089182887937, RegionCode: "MM", Countries: []string{"MM"}},
{AppCode: "aslan", RegionID: 2049040320781811714, RegionCode: "PK", Countries: []string{"PK", "MM"}},
}
regionID, found, err := regionIDForCountryCodeInRegions(regions, "MM")
if err != nil || !found || regionID != 2064044089182887937 {
t.Fatalf("MM should resolve to the newest region: id=%d found=%v err=%v", regionID, found, err)
}
regionID, found, err = regionIDForCountryCodeInRegions(regions, "PK")
if err != nil || !found || regionID != 2049040320781811714 {
t.Fatalf("PK region mismatch: id=%d found=%v err=%v", regionID, found, err)
}
if _, found, _ = regionIDForCountryCodeInRegions(regions, "BR"); found {
t.Fatalf("unmapped country should not resolve")
}
}
type staticCountryRegionResolver struct {
staticMoneyRegionSource
appCode string
}
func (s *staticCountryRegionResolver) RegionIDForCountryCode(_ context.Context, appCode string, countryCode string) (int64, bool, error) {
if appCode != s.appCode {
return 0, false, nil
}
return regionIDForCountryCodeInRegions(s.regions, strings.ToUpper(strings.TrimSpace(countryCode)))
}
func TestRegionIDForCountryCodeSkipsSourcesWithoutResolver(t *testing.T) {
sources := []MoneyRegionSource{
&staticMoneyRegionSource{},
&staticCountryRegionResolver{
appCode: "yumi",
staticMoneyRegionSource: staticMoneyRegionSource{
regions: []moneyRegionDTO{{AppCode: "yumi", RegionID: 1002, RegionCode: "INDIA", Countries: []string{"IN"}}},
},
},
}
regionID, found, err := RegionIDForCountryCode(context.Background(), sources, "yumi", "IN")
if err != nil || !found || regionID != 1002 {
t.Fatalf("cross-source resolve mismatch: id=%d found=%v err=%v", regionID, found, err)
}
if _, found, _ = RegionIDForCountryCode(context.Background(), sources, "aslan", "IN"); found {
t.Fatalf("other app should not resolve")
}
}