231 lines
8.9 KiB
Go
231 lines
8.9 KiB
Go
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")
|
||
}
|
||
}
|