465 lines
22 KiB
Go
465 lines
22 KiB
Go
package policyconfig
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
mysqlDriver "github.com/go-sql-driver/mysql"
|
|
)
|
|
|
|
func TestGetTemplateJoinsVersionByTemplateCodeAndVersion(t *testing.T) {
|
|
adminDB, mock, closeDB := newPolicySQLMock(t)
|
|
defer closeDB()
|
|
svc := NewService(adminDB, nil, nil)
|
|
|
|
mock.ExpectQuery(`(?s)JOIN admin_policy_template_versions v ON v\.template_code = t\.template_code AND v\.template_version = t\.template_version.*WHERE t\.template_code = \? AND v\.template_version = \?`).
|
|
WithArgs("huwaa_point_policy", "v2").
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"template_id",
|
|
"version_id",
|
|
"template_code",
|
|
"template_version",
|
|
"name",
|
|
"status",
|
|
"rule_json",
|
|
"description",
|
|
"created_at_ms",
|
|
"updated_at_ms",
|
|
}).AddRow(1, 22, "huwaa_point_policy", "v2", "Huwaa Point Policy", "active", `{"host":{"point_ratio_percent":70},"tasks":{"reward_asset_type":"POINT"}}`, "version scoped", int64(1700000000000), int64(1700000001000)))
|
|
|
|
got, err := svc.getTemplate(context.Background(), "huwaa_point_policy", "v2")
|
|
if err != nil {
|
|
t.Fatalf("getTemplate failed: %v", err)
|
|
}
|
|
if got.TemplateCode != "huwaa_point_policy" || got.TemplateVersion != "v2" || got.VersionID != 22 {
|
|
t.Fatalf("template version join result mismatch: %+v", got)
|
|
}
|
|
if string(got.RuleJSON) != `{}` {
|
|
t.Fatalf("template DTO must hide retired direct-gift and task-default fields: %s", got.RuleJSON)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("admin sql expectations mismatch: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPublishRuntimeWritesWalletRows(t *testing.T) {
|
|
walletDB, walletMock, closeWallet := newPolicySQLMock(t)
|
|
defer closeWallet()
|
|
svc := NewService(nil, walletDB, nil)
|
|
ctx := context.Background()
|
|
nowMS := int64(1700000005000)
|
|
item := policyInstanceRow{
|
|
AppCode: "huwaa",
|
|
InstanceCode: "huwaa-point-live",
|
|
TemplateCode: "huwaa_point_policy",
|
|
TemplateVersion: "v2",
|
|
Status: policyStatusActive,
|
|
EffectiveFromMS: 1700000000000,
|
|
EffectiveToMS: 0,
|
|
}
|
|
compiled := compiledPolicy{
|
|
PointsPerUSD: 100000,
|
|
MinimumPoints: 1000000,
|
|
WithdrawFeeBPS: 500,
|
|
RuleJSON: json.RawMessage(`{"points_per_usd":100000,"host":{"minimum_withdraw_points":1000000,"withdraw_fee_bps":500}}`),
|
|
}
|
|
|
|
walletMock.ExpectExec(`(?s)CREATE TABLE IF NOT EXISTS wallet_policy_instances`).WillReturnResult(sqlmock.NewResult(0, 0))
|
|
walletMock.ExpectExec(`(?s)ALTER TABLE wallet_policy_instances ADD COLUMN agency_point_ratio_ppm`).WillReturnResult(sqlmock.NewResult(0, 0))
|
|
walletMock.ExpectBegin()
|
|
walletMock.ExpectExec(`DELETE FROM wallet_policy_instances WHERE app_code = \? AND instance_code = \?`).
|
|
WithArgs("huwaa", "huwaa-point-live").
|
|
WillReturnResult(sqlmock.NewResult(0, 2))
|
|
for _, regionID := range []int64{0, 12} {
|
|
walletMock.ExpectExec(`(?s)INSERT INTO wallet_policy_instances`).
|
|
WithArgs(
|
|
"huwaa",
|
|
"huwaa-point-live",
|
|
"huwaa_point_policy",
|
|
"v2",
|
|
regionID,
|
|
policyStatusActive,
|
|
int64(1700000000000),
|
|
int64(0),
|
|
int64(0),
|
|
int64(0),
|
|
int64(100000),
|
|
int64(500),
|
|
string(compiled.RuleJSON),
|
|
uint(90001),
|
|
nowMS,
|
|
nowMS,
|
|
nowMS,
|
|
).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
}
|
|
walletMock.ExpectCommit()
|
|
|
|
if err := svc.publishWalletRuntime(ctx, item, compiled, []int64{0, 12}, 90001, nowMS); err != nil {
|
|
t.Fatalf("publishWalletRuntime failed: %v", err)
|
|
}
|
|
if err := walletMock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("wallet sql expectations mismatch: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInsertPublishItemsRecordsWalletTargetsOnly(t *testing.T) {
|
|
adminDB, mock, closeDB := newPolicySQLMock(t)
|
|
defer closeDB()
|
|
svc := NewService(adminDB, nil, nil)
|
|
nowMS := int64(1700000005000)
|
|
|
|
mock.ExpectBegin()
|
|
for _, regionID := range []int64{0, 12} {
|
|
mock.ExpectExec(`(?s)INSERT INTO admin_policy_publish_items.*VALUES \(\?, 'wallet', \?, \?, 'succeeded', \?, \?\)`).
|
|
WithArgs(uint64(77), "configured-app", regionID, nowMS, nowMS).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
}
|
|
mock.ExpectCommit()
|
|
|
|
if err := svc.insertPublishItems(context.Background(), 77, "configured-app", []int64{0, 12}, nowMS); err != nil {
|
|
t.Fatalf("insert wallet-only publish items failed: %v", err)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("publish items must not include the retired App-global activity target: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateInstanceStatusSyncsWalletRuntime(t *testing.T) {
|
|
adminDB, adminMock, closeAdmin := newPolicySQLMock(t)
|
|
defer closeAdmin()
|
|
walletDB, walletMock, closeWallet := newPolicySQLMock(t)
|
|
defer closeWallet()
|
|
svc := NewService(adminDB, walletDB, nil)
|
|
ctx := context.Background()
|
|
|
|
adminMock.ExpectQuery(`(?s)SELECT instance_id, app_code, instance_code, template_code, template_version, region_scope, status,\s+effective_from_ms, effective_to_ms\s+FROM admin_policy_instances\s+WHERE app_code = \? AND instance_id = \?`).
|
|
WithArgs("huwaa", uint64(9)).
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"instance_id",
|
|
"app_code",
|
|
"instance_code",
|
|
"template_code",
|
|
"template_version",
|
|
"region_scope",
|
|
"status",
|
|
"effective_from_ms",
|
|
"effective_to_ms",
|
|
}).AddRow(9, "huwaa", "huwaa-point-live", "huwaa_point_policy", "v2", "all_active_regions", policyStatusActive, int64(1700000000000), int64(0)))
|
|
adminMock.ExpectExec(`(?s)UPDATE admin_policy_instances\s+SET status = \?, updated_by_admin_id = \?, updated_at_ms = \?\s+WHERE app_code = \? AND instance_id = \?`).
|
|
WithArgs(policyStatusDisabled, uint(90002), sqlmock.AnyArg(), "huwaa", uint64(9)).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
walletMock.ExpectExec(`(?s)CREATE TABLE IF NOT EXISTS wallet_policy_instances`).WillReturnResult(sqlmock.NewResult(0, 0))
|
|
walletMock.ExpectExec(`(?s)ALTER TABLE wallet_policy_instances ADD COLUMN agency_point_ratio_ppm`).WillReturnResult(sqlmock.NewResult(0, 0))
|
|
walletMock.ExpectExec(`(?s)UPDATE wallet_policy_instances\s+SET status = \?, updated_at_ms = \?\s+WHERE app_code = \? AND instance_code = \?`).
|
|
WithArgs(policyStatusDisabled, sqlmock.AnyArg(), "huwaa", "huwaa-point-live").
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
adminMock.ExpectQuery(`(?s)SELECT instance_id, app_code, instance_code, template_code, template_version, region_scope, status,\s+effective_from_ms, effective_to_ms, publish_status, publish_error, last_published_at_ms,\s+created_at_ms, updated_at_ms\s+FROM admin_policy_instances\s+WHERE app_code = \? AND instance_id = \?`).
|
|
WithArgs("huwaa", uint64(9)).
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"instance_id",
|
|
"app_code",
|
|
"instance_code",
|
|
"template_code",
|
|
"template_version",
|
|
"region_scope",
|
|
"status",
|
|
"effective_from_ms",
|
|
"effective_to_ms",
|
|
"publish_status",
|
|
"publish_error",
|
|
"last_published_at_ms",
|
|
"created_at_ms",
|
|
"updated_at_ms",
|
|
}).AddRow(9, "huwaa", "huwaa-point-live", "huwaa_point_policy", "v2", "all_active_regions", policyStatusDisabled, int64(1700000000000), int64(0), publishStatusPublished, "", int64(1700000005000), int64(1700000000000), int64(1700000006000)))
|
|
|
|
got, err := svc.UpdateInstanceStatus(ctx, " Huwaa ", 90002, 9, "disabled")
|
|
if err != nil {
|
|
t.Fatalf("UpdateInstanceStatus failed: %v", err)
|
|
}
|
|
if got.Status != policyStatusDisabled {
|
|
t.Fatalf("instance status mismatch: %+v", got)
|
|
}
|
|
if err := adminMock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("admin sql expectations mismatch: %v", err)
|
|
}
|
|
if err := walletMock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("wallet sql expectations mismatch: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCompilePolicyRetainsOnlyPointFinancialFields(t *testing.T) {
|
|
ruleJSON := json.RawMessage(`{"points_per_usd":120000,"google_coin_per_usd":70000,"allow_self_brushing":true,"host":{"point_ratio_percent":70,"use_gift_type_ratio":true,"affects_room_heat":true,"minimum_withdraw_points":3000000,"withdraw_fee_bps":250,"future_host_option":"keep"},"agency":{"point_ratio_percent":20},"agent":{"levels":[]},"bd":{"levels":[]},"manager":{"host_point_commission_percent":2,"future_manager_option":"keep"},"coin_seller":{"levels":[],"seller_point_reward_percent":5,"seller_point_settle_percent":95},"tasks":{"reward_asset_type":"POINT","new_host_7d_max_points":350000},"game_invite":{"enabled":true},"extension":{"enabled":true}}`)
|
|
compiled, err := compilePolicy(ruleJSON)
|
|
if err != nil {
|
|
t.Fatalf("compile POINT policy failed: %v", err)
|
|
}
|
|
if compiled.PointsPerUSD != 120000 || compiled.MinimumPoints != 3000000 || compiled.WithdrawFeeBPS != 250 {
|
|
t.Fatalf("retained POINT financial fields mismatch: %+v", compiled)
|
|
}
|
|
normalized, err := normalizeRuleJSON(ruleJSON)
|
|
if err != nil {
|
|
t.Fatalf("normalize POINT policy failed: %v", err)
|
|
}
|
|
if string(normalized) != `{"host":{"minimum_withdraw_points":3000000,"withdraw_fee_bps":250},"points_per_usd":120000}` {
|
|
t.Fatalf("normalized policy must contain only runtime POINT fields: %s", normalized)
|
|
}
|
|
}
|
|
|
|
func TestCompilePolicyIgnoresInvalidRetiredDirectGiftFieldTypes(t *testing.T) {
|
|
ruleJSON := json.RawMessage(`{"host":{"point_ratio_percent":"obsolete","use_gift_type_ratio":"obsolete","minimum_withdraw_points":1000000,"withdraw_fee_bps":500},"agency":{"point_ratio_percent":{},"share_base":123},"points_per_usd":100000}`)
|
|
compiled, err := compilePolicy(ruleJSON)
|
|
if err != nil {
|
|
t.Fatalf("retired direct-gift fields must no longer participate in validation: %v", err)
|
|
}
|
|
if compiled.MinimumPoints != 1_000_000 {
|
|
t.Fatalf("explicit minimum withdrawal points mismatch: %+v", compiled)
|
|
}
|
|
normalized, err := normalizeRuleJSON(ruleJSON)
|
|
if err != nil {
|
|
t.Fatalf("normalize retired fields failed: %v", err)
|
|
}
|
|
if string(normalized) != `{"host":{"minimum_withdraw_points":1000000,"withdraw_fee_bps":500},"points_per_usd":100000}` {
|
|
t.Fatalf("retired direct-gift fields must be absent from normalized rule_json: %s", normalized)
|
|
}
|
|
}
|
|
|
|
func TestCompilePolicyRejectsInvalidMinimumWithdrawalPoints(t *testing.T) {
|
|
for _, ruleJSON := range []json.RawMessage{
|
|
json.RawMessage(`{"points_per_usd":100000,"host":{"minimum_withdraw_points":0,"withdraw_fee_bps":500}}`),
|
|
json.RawMessage(`{"points_per_usd":100000,"host":{"minimum_withdraw_points":-1,"withdraw_fee_bps":500}}`),
|
|
json.RawMessage(`{"points_per_usd":100000,"host":{"minimum_withdraw_points":1.5,"withdraw_fee_bps":500}}`),
|
|
json.RawMessage(`{"points_per_usd":100000,"host":{"minimum_withdraw_points":"invalid","withdraw_fee_bps":500}}`),
|
|
} {
|
|
if _, err := compilePolicy(ruleJSON); err == nil {
|
|
t.Fatalf("invalid minimum withdrawal points must be rejected: %s", ruleJSON)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompilePolicyRequiresEveryPointFinancialFieldExplicitly(t *testing.T) {
|
|
for _, ruleJSON := range []json.RawMessage{
|
|
json.RawMessage(`{}`),
|
|
json.RawMessage(`{"host":{"minimum_withdraw_points":1000000,"withdraw_fee_bps":500}}`),
|
|
json.RawMessage(`{"points_per_usd":100000,"host":{"withdraw_fee_bps":500}}`),
|
|
json.RawMessage(`{"points_per_usd":100000,"host":{"minimum_withdraw_points":1000000}}`),
|
|
json.RawMessage(`{"points_per_usd":0,"host":{"minimum_withdraw_points":1000000,"withdraw_fee_bps":500}}`),
|
|
json.RawMessage(`{"points_per_usd":100000,"host":{"minimum_withdraw_points":1000000,"withdraw_fee_bps":-1}}`),
|
|
json.RawMessage(`{"points_per_usd":100000,"host":{"minimum_withdraw_points":1000000,"withdraw_fee_bps":10001}}`),
|
|
} {
|
|
if _, err := compilePolicy(ruleJSON); err == nil {
|
|
t.Fatalf("incomplete or invalid POINT policy must not publish: %s", ruleJSON)
|
|
}
|
|
}
|
|
|
|
compiled, err := compilePolicy(json.RawMessage(`{"points_per_usd":100000,"host":{"minimum_withdraw_points":1000000,"withdraw_fee_bps":0}}`))
|
|
if err != nil || compiled.WithdrawFeeBPS != 0 {
|
|
t.Fatalf("explicit zero withdrawal fee must remain valid: compiled=%+v err=%v", compiled, err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeRuleJSONDropsEmptyRetiredSections(t *testing.T) {
|
|
normalized, err := normalizeRuleJSON(json.RawMessage(`{"host":{"point_ratio_percent":70,"use_gift_type_ratio":true},"agency":{"point_ratio_percent":20,"share_base":"charge_amount"},"tasks":{"reward_asset_type":"COIN","new_host_7d_max_points":350000}}`))
|
|
if err != nil {
|
|
t.Fatalf("normalize rule_json failed: %v", err)
|
|
}
|
|
if string(normalized) != `{}` {
|
|
t.Fatalf("empty retired sections must not remain in policy DTO/runtime snapshot: %s", normalized)
|
|
}
|
|
}
|
|
|
|
func TestRemoveDirectGiftPointPolicyMigrationIsScopedAndIdempotent(t *testing.T) {
|
|
baseDSN := strings.TrimSpace(os.Getenv("POLICY_CONFIG_MYSQL_TEST_DSN"))
|
|
if baseDSN == "" {
|
|
baseDSN = strings.TrimSpace(os.Getenv("WALLET_SERVICE_MYSQL_TEST_DSN"))
|
|
}
|
|
if baseDSN == "" {
|
|
t.Skip("set POLICY_CONFIG_MYSQL_TEST_DSN to run policy migration regression")
|
|
}
|
|
db := newPolicyMigrationMySQLDB(t, baseDSN)
|
|
execPolicyMigrationSQL(t, db, `
|
|
CREATE TABLE admin_policy_templates (
|
|
template_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
template_code VARCHAR(96) NOT NULL,
|
|
rule_json JSON NOT NULL,
|
|
PRIMARY KEY (template_id),
|
|
UNIQUE KEY uk_template_code (template_code)
|
|
);
|
|
CREATE TABLE admin_policy_template_versions (
|
|
version_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
template_code VARCHAR(96) NOT NULL,
|
|
template_version VARCHAR(64) NOT NULL,
|
|
rule_json JSON NOT NULL,
|
|
PRIMARY KEY (version_id),
|
|
UNIQUE KEY uk_template_version (template_code, template_version),
|
|
KEY idx_admin_policy_template_versions_status (template_code)
|
|
);
|
|
CREATE TABLE admin_policy_instances (
|
|
instance_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
app_code VARCHAR(32) NOT NULL,
|
|
instance_code VARCHAR(96) NOT NULL,
|
|
template_code VARCHAR(96) NOT NULL,
|
|
template_version VARCHAR(64) NOT NULL,
|
|
PRIMARY KEY (instance_id),
|
|
KEY idx_admin_policy_instances_template (template_code, template_version)
|
|
);
|
|
CREATE TABLE admin_policy_publish_jobs (
|
|
job_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
instance_id BIGINT UNSIGNED NOT NULL,
|
|
template_code VARCHAR(96) NOT NULL,
|
|
PRIMARY KEY (job_id),
|
|
KEY idx_admin_policy_publish_jobs_instance (instance_id)
|
|
);
|
|
CREATE TABLE admin_policy_publish_items (
|
|
item_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
job_id BIGINT UNSIGNED NOT NULL,
|
|
PRIMARY KEY (item_id),
|
|
KEY idx_admin_policy_publish_items_job (job_id)
|
|
);
|
|
CREATE TABLE admin_menus (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
title VARCHAR(80) NOT NULL,
|
|
code VARCHAR(100) NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY uk_admin_menus_code (code)
|
|
);
|
|
CREATE TABLE admin_permissions (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
name VARCHAR(100) NOT NULL,
|
|
code VARCHAR(100) NOT NULL,
|
|
description VARCHAR(255) NOT NULL,
|
|
updated_at_ms BIGINT NOT NULL,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY uk_admin_permissions_code (code)
|
|
);
|
|
SET @policy_rule = JSON_OBJECT(
|
|
'points_per_usd', 120000,
|
|
'host', JSON_OBJECT('minimum_withdraw_points', 3000000, 'withdraw_fee_bps', 250, 'point_ratio_percent', 70, 'use_gift_type_ratio', TRUE, 'affects_room_heat', TRUE),
|
|
'agency', JSON_OBJECT('point_ratio_percent', 20, 'share_base', 'host_income'),
|
|
'agent', JSON_OBJECT('levels', JSON_ARRAY(JSON_OBJECT('level', 1))),
|
|
'bd', JSON_OBJECT('levels', JSON_ARRAY(JSON_OBJECT('level', 1))),
|
|
'manager', JSON_OBJECT('host_point_commission_percent', 2, 'future_manager_option', 'keep'),
|
|
'coin_seller', JSON_OBJECT('seller_point_reward_percent', 5, 'seller_point_settle_percent', 95, 'future_coin_seller_option', 'keep'),
|
|
'game_invite', JSON_OBJECT('enabled', TRUE),
|
|
'tasks', JSON_OBJECT('reward_asset_type', 'POINT', 'new_host_7d_max_points', 350000),
|
|
'extension', JSON_OBJECT('enabled', TRUE)
|
|
);
|
|
INSERT INTO admin_policy_templates (template_id, template_code, rule_json) VALUES
|
|
(1, 'fami_guild_revenue_policy', @policy_rule),
|
|
(2, 'unrelated_point_policy', @policy_rule);
|
|
INSERT INTO admin_policy_template_versions (version_id, template_code, template_version, rule_json) VALUES
|
|
(1, 'fami_guild_revenue_policy', 'v1', @policy_rule),
|
|
(2, 'unrelated_point_policy', 'v1', @policy_rule);
|
|
INSERT INTO admin_policy_instances (instance_id, app_code, instance_code, template_code, template_version) VALUES
|
|
(1, 'fami', 'fami-policy', 'fami_guild_revenue_policy', 'v1'),
|
|
(2, 'lalu', 'lalu-policy', 'unrelated_point_policy', 'v1');
|
|
INSERT INTO admin_policy_publish_jobs (job_id, instance_id, template_code) VALUES
|
|
(1, 1, 'fami_guild_revenue_policy'),
|
|
(2, 2, 'unrelated_point_policy');
|
|
INSERT INTO admin_policy_publish_items (item_id, job_id) VALUES (1, 1), (2, 2);
|
|
INSERT INTO admin_menus (id, title, code, updated_at_ms) VALUES
|
|
(1, '收益政策模板', 'policy-template', 0),
|
|
(2, '其他菜单', 'other-menu', 0);
|
|
INSERT INTO admin_permissions (id, name, code, description, updated_at_ms) VALUES
|
|
(1, '收益政策模板查看', 'policy-template:view', '旧文案', 0),
|
|
(2, '其他权限', 'other:view', '其他文案', 0);
|
|
`)
|
|
|
|
migrationSQL, err := os.ReadFile("../../../migrations/116_remove_direct_gift_point_policy.sql")
|
|
if err != nil {
|
|
t.Fatalf("read policy removal migration failed: %v", err)
|
|
}
|
|
// 重复在同一个真实 MySQL schema 执行,确保子表清理顺序与幂等性都由数据库验证。
|
|
execPolicyMigrationSQL(t, db, string(migrationSQL))
|
|
execPolicyMigrationSQL(t, db, string(migrationSQL))
|
|
|
|
for _, assertion := range []struct {
|
|
name string
|
|
query string
|
|
want int
|
|
}{
|
|
{name: "removed template", query: `SELECT COUNT(*) FROM admin_policy_templates WHERE template_code = 'fami_guild_revenue_policy'`, want: 0},
|
|
{name: "removed version", query: `SELECT COUNT(*) FROM admin_policy_template_versions WHERE template_code = 'fami_guild_revenue_policy'`, want: 0},
|
|
{name: "removed instance", query: `SELECT COUNT(*) FROM admin_policy_instances WHERE template_code = 'fami_guild_revenue_policy'`, want: 0},
|
|
{name: "removed job", query: `SELECT COUNT(*) FROM admin_policy_publish_jobs WHERE template_code = 'fami_guild_revenue_policy'`, want: 0},
|
|
{name: "removed item", query: `SELECT COUNT(*) FROM admin_policy_publish_items WHERE item_id = 1`, want: 0},
|
|
{name: "unrelated template", query: `SELECT COUNT(*) FROM admin_policy_templates WHERE template_code = 'unrelated_point_policy'`, want: 1},
|
|
{name: "unrelated version", query: `SELECT COUNT(*) FROM admin_policy_template_versions WHERE template_code = 'unrelated_point_policy'`, want: 1},
|
|
{name: "unrelated instance", query: `SELECT COUNT(*) FROM admin_policy_instances WHERE template_code = 'unrelated_point_policy'`, want: 1},
|
|
{name: "unrelated job", query: `SELECT COUNT(*) FROM admin_policy_publish_jobs WHERE template_code = 'unrelated_point_policy'`, want: 1},
|
|
{name: "unrelated item", query: `SELECT COUNT(*) FROM admin_policy_publish_items WHERE item_id = 2`, want: 1},
|
|
{name: "ghost sections removed", query: `SELECT COUNT(*) FROM admin_policy_templates WHERE template_code = 'unrelated_point_policy' AND JSON_EXTRACT(rule_json, '$.tasks') IS NULL AND JSON_EXTRACT(rule_json, '$.host.point_ratio_percent') IS NULL AND JSON_EXTRACT(rule_json, '$.agency') IS NULL AND JSON_EXTRACT(rule_json, '$.agent') IS NULL AND JSON_EXTRACT(rule_json, '$.bd') IS NULL AND JSON_EXTRACT(rule_json, '$.manager') IS NULL AND JSON_EXTRACT(rule_json, '$.coin_seller') IS NULL AND JSON_EXTRACT(rule_json, '$.game_invite') IS NULL`, want: 1},
|
|
{name: "financial and unrelated extension retained", query: `SELECT COUNT(*) FROM admin_policy_templates WHERE template_code = 'unrelated_point_policy' AND JSON_EXTRACT(rule_json, '$.points_per_usd') = 120000 AND JSON_EXTRACT(rule_json, '$.host.minimum_withdraw_points') = 3000000 AND JSON_EXTRACT(rule_json, '$.host.withdraw_fee_bps') = 250 AND JSON_EXTRACT(rule_json, '$.extension.enabled') = TRUE`, want: 1},
|
|
{name: "menu title updated", query: `SELECT COUNT(*) FROM admin_menus WHERE code = 'policy-template' AND title = 'POINT 钱包政策'`, want: 1},
|
|
{name: "permission title updated", query: `SELECT COUNT(*) FROM admin_permissions WHERE code = 'policy-template:view' AND name = 'POINT 钱包政策查看' AND description = '允许查看 POINT 钱包政策和实例'`, want: 1},
|
|
{name: "other menu unchanged", query: `SELECT COUNT(*) FROM admin_menus WHERE code = 'other-menu' AND title = '其他菜单'`, want: 1},
|
|
{name: "other permission unchanged", query: `SELECT COUNT(*) FROM admin_permissions WHERE code = 'other:view' AND name = '其他权限' AND description = '其他文案'`, want: 1},
|
|
} {
|
|
var got int
|
|
if err := db.QueryRow(assertion.query).Scan(&got); err != nil {
|
|
t.Fatalf("query %s failed: %v", assertion.name, err)
|
|
}
|
|
if got != assertion.want {
|
|
t.Fatalf("%s mismatch: got=%d want=%d", assertion.name, got, assertion.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newPolicyMigrationMySQLDB(t *testing.T, baseDSN string) *sql.DB {
|
|
t.Helper()
|
|
baseConfig, err := mysqlDriver.ParseDSN(baseDSN)
|
|
if err != nil {
|
|
t.Fatalf("parse policy migration MySQL DSN failed: %v", err)
|
|
}
|
|
databaseName := fmt.Sprintf("hy_policy_remove_%d", time.Now().UnixNano())
|
|
adminConfig := baseConfig.Clone()
|
|
adminConfig.DBName = ""
|
|
adminDB, err := sql.Open("mysql", adminConfig.FormatDSN())
|
|
if err != nil {
|
|
t.Fatalf("open policy migration admin DB failed: %v", err)
|
|
}
|
|
if _, err := adminDB.Exec("CREATE DATABASE `" + databaseName + "` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); err != nil {
|
|
_ = adminDB.Close()
|
|
t.Fatalf("create policy migration database failed: %v", err)
|
|
}
|
|
testConfig := baseConfig.Clone()
|
|
testConfig.DBName = databaseName
|
|
testConfig.MultiStatements = true
|
|
db, err := sql.Open("mysql", testConfig.FormatDSN())
|
|
if err != nil {
|
|
_, _ = adminDB.Exec("DROP DATABASE IF EXISTS `" + databaseName + "`")
|
|
_ = adminDB.Close()
|
|
t.Fatalf("open policy migration test DB failed: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = db.Close()
|
|
_, _ = adminDB.Exec("DROP DATABASE IF EXISTS `" + databaseName + "`")
|
|
_ = adminDB.Close()
|
|
})
|
|
return db
|
|
}
|
|
|
|
func execPolicyMigrationSQL(t *testing.T, db *sql.DB, script string) {
|
|
t.Helper()
|
|
if _, err := db.Exec(script); err != nil {
|
|
t.Fatalf("execute policy migration SQL failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func newPolicySQLMock(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() }
|
|
}
|