chore: snapshot current aws standby and backup tooling work
This commit is contained in:
@@ -1,198 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNormalizeEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
// Gmail dot trick
|
||||
{"a.v.x.xx@gmail.com", "avxxx@gmail.com"},
|
||||
{"john.doe@gmail.com", "johndoe@gmail.com"},
|
||||
{"a.b.c.d.e@gmail.com", "abcde@gmail.com"},
|
||||
// Gmail + alias
|
||||
{"user+tag@gmail.com", "user@gmail.com"},
|
||||
{"a.b+tag@gmail.com", "ab@gmail.com"},
|
||||
// Googlemail
|
||||
{"a.b@googlemail.com", "ab@googlemail.com"},
|
||||
// Non-Gmail: dots preserved
|
||||
{"john.doe@outlook.com", "john.doe@outlook.com"},
|
||||
{"john.doe@qq.com", "john.doe@qq.com"},
|
||||
// + alias stripped for all providers
|
||||
{"user+spam@outlook.com", "user@outlook.com"},
|
||||
{"user+spam@qq.com", "user@qq.com"},
|
||||
// Case insensitive
|
||||
{"User@Gmail.COM", "user@gmail.com"},
|
||||
{"A.B@Gmail.com", "ab@gmail.com"},
|
||||
// No change for normal non-gmail email
|
||||
{"abc@163.com", "abc@163.com"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := NormalizeEmail(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("NormalizeEmail(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeEmail_NoChangeSkipsCheck(t *testing.T) {
|
||||
// These emails should NOT trigger cross-user check (normalized == original)
|
||||
noChangeCases := []string{
|
||||
"abc@163.com",
|
||||
"john.doe@outlook.com",
|
||||
"user@qq.com",
|
||||
}
|
||||
for _, email := range noChangeCases {
|
||||
normalized := NormalizeEmail(email)
|
||||
lower := email
|
||||
if normalized == lower {
|
||||
// correct: no normalization change, NormalizedEmailHasTrial would return false early
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGrantTrialForEmail(t *testing.T) {
|
||||
// 模拟线上配置:白名单开启,gmail.com 也在名单里
|
||||
rcWithGmail := config.RegisterConfig{
|
||||
EnableTrial: true,
|
||||
EnableTrialEmailWhitelist: true,
|
||||
TrialEmailDomainWhitelist: "hifastapp.com,hifastvpn.com,126.com,139.com,163.com,gmail.com",
|
||||
}
|
||||
// 白名单关闭
|
||||
rcNoWhitelist := config.RegisterConfig{
|
||||
EnableTrial: true,
|
||||
EnableTrialEmailWhitelist: false,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rc config.RegisterConfig
|
||||
email string
|
||||
want bool
|
||||
reason string
|
||||
}{
|
||||
{
|
||||
name: "gmail dot trick - blocked even if gmail.com in whitelist",
|
||||
rc: rcWithGmail,
|
||||
email: "s.m.s.n.fsmbt.d.ndny@gmail.com",
|
||||
want: false,
|
||||
reason: "gmail 泛域名(含点号)应拒绝",
|
||||
},
|
||||
{
|
||||
name: "gmail plus alias - blocked",
|
||||
rc: rcWithGmail,
|
||||
email: "user+tag@gmail.com",
|
||||
want: false,
|
||||
reason: "gmail +别名应拒绝",
|
||||
},
|
||||
{
|
||||
name: "clean gmail - allowed",
|
||||
rc: rcWithGmail,
|
||||
email: "normaluser@gmail.com",
|
||||
want: true,
|
||||
reason: "干净的 gmail 应放行",
|
||||
},
|
||||
{
|
||||
name: "163 with dot - allowed (non-gmail dot is ok)",
|
||||
rc: rcWithGmail,
|
||||
email: "s.m.s.n@163.com",
|
||||
want: true,
|
||||
reason: "非 gmail 域点号不拦截",
|
||||
},
|
||||
{
|
||||
name: "163 plus alias - blocked",
|
||||
rc: rcWithGmail,
|
||||
email: "user+spam@163.com",
|
||||
want: false,
|
||||
reason: "所有域名的 +别名都拦截",
|
||||
},
|
||||
{
|
||||
name: "gmail typo squatting domain - blocked even if accidentally whitelisted",
|
||||
rc: config.RegisterConfig{EnableTrial: true, EnableTrialEmailWhitelist: true, TrialEmailDomainWhitelist: "gmail.com,gmaial.com"},
|
||||
email: "1.2.3.4xxx@gmaial.com",
|
||||
want: false,
|
||||
reason: "易混淆 Gmail 域名不应发放试用",
|
||||
},
|
||||
{
|
||||
name: "invalid empty local - blocked",
|
||||
rc: rcWithGmail,
|
||||
email: "@gmail.com",
|
||||
want: false,
|
||||
reason: "邮箱 local 为空应拒绝",
|
||||
},
|
||||
{
|
||||
name: "subdomain spoof - blocked",
|
||||
rc: rcWithGmail,
|
||||
email: "user@fake.gmail.com",
|
||||
want: false,
|
||||
reason: "白名单必须精确匹配域名,不匹配子域",
|
||||
},
|
||||
{
|
||||
name: "whitelist disabled - gmail dot trick still blocked",
|
||||
rc: rcNoWhitelist,
|
||||
email: "s.m.s.n.fsmbt.d.ndny@gmail.com",
|
||||
want: false,
|
||||
reason: "白名单未启用,但泛域名仍应拒绝",
|
||||
},
|
||||
{
|
||||
name: "trial disabled - always blocked",
|
||||
rc: config.RegisterConfig{EnableTrial: false},
|
||||
email: "user@163.com",
|
||||
want: false,
|
||||
reason: "试用未开启",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ShouldGrantTrialForEmail(tt.rc, tt.email)
|
||||
if got != tt.want {
|
||||
t.Errorf("ShouldGrantTrialForEmail(%q) = %v, want %v | reason: %s",
|
||||
tt.email, got, tt.want, tt.reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldAutoGrantTrialOnPublicEmailFlows(t *testing.T) {
|
||||
assert.False(t, ShouldAutoGrantTrialOnPublicEmailFlows(config.RegisterConfig{}))
|
||||
assert.False(t, ShouldAutoGrantTrialOnPublicEmailFlows(config.RegisterConfig{
|
||||
EnableTrial: true,
|
||||
EnableTrialEmailWhitelist: true,
|
||||
TrialEmailDomainWhitelist: "gmail.com,example.com",
|
||||
}))
|
||||
}
|
||||
|
||||
func TestIsEmailDomainWhitelisted(t *testing.T) {
|
||||
whitelist := "gmail.com,edu.cn,outlook.com"
|
||||
tests := []struct {
|
||||
email string
|
||||
want bool
|
||||
}{
|
||||
{"user@gmail.com", true},
|
||||
{"user@edu.cn", true},
|
||||
{"User@Gmail.COM", true},
|
||||
{"user@yahoo.com", false},
|
||||
{"user@fake.gmail.com", false}, // subdomain not matched
|
||||
{"user@", false},
|
||||
{"notanemail", false},
|
||||
{"@gmail.com", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.email, func(t *testing.T) {
|
||||
got := IsEmailDomainWhitelisted(tt.email, whitelist)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsEmailDomainWhitelisted(%q) = %v, want %v", tt.email, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolvePurchaseRoute_AllowsPlanChangeForExistingSubscription(t *testing.T) {
|
||||
anchor := &user.Subscribe{
|
||||
Id: 10,
|
||||
UserId: 20,
|
||||
OrderId: 30,
|
||||
SubscribeId: 1,
|
||||
Token: "existing-token",
|
||||
ExpireTime: time.Now().Add(time.Hour),
|
||||
}
|
||||
|
||||
decision, err := ResolvePurchaseRoute(
|
||||
context.Background(),
|
||||
true,
|
||||
anchor.UserId,
|
||||
2,
|
||||
func(context.Context, int64) (*user.Subscribe, error) {
|
||||
return anchor, nil
|
||||
},
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, PurchaseRoutePurchaseToRenewal, decision.Route)
|
||||
require.Equal(t, int64(2), decision.ResolvedSubscribeID)
|
||||
require.Equal(t, anchor, decision.Anchor)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetDiscount_SkipsNewUserOnlyTierForExistingUser(t *testing.T) {
|
||||
discount := getDiscount([]types.SubscribeDiscount{
|
||||
{Quantity: 1, Discount: 90, NewUserOnly: true},
|
||||
}, 1, false)
|
||||
|
||||
require.Equal(t, float64(1), discount)
|
||||
}
|
||||
@@ -1,845 +0,0 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/hibiken/asynq"
|
||||
modelOrder "github.com/perfect-panel/server/internal/model/order"
|
||||
"github.com/perfect-panel/server/internal/model/payment"
|
||||
subModel "github.com/perfect-panel/server/internal/model/subscribe"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// setupNewUserOnlyDB 创建带必要表的 SQLite 内存数据库
|
||||
func setupNewUserOnlyDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
require.NoError(t, err, "failed to open in-memory SQLite")
|
||||
db.Exec("PRAGMA foreign_keys = OFF")
|
||||
|
||||
sqls := []string{
|
||||
`CREATE TABLE IF NOT EXISTS "subscribe" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
language VARCHAR(255) NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
unit_price INTEGER NOT NULL DEFAULT 0,
|
||||
unit_time VARCHAR(255) NOT NULL DEFAULT '',
|
||||
discount TEXT,
|
||||
replacement INTEGER NOT NULL DEFAULT 0,
|
||||
inventory INTEGER NOT NULL DEFAULT -1,
|
||||
traffic INTEGER NOT NULL DEFAULT 0,
|
||||
speed_limit INTEGER NOT NULL DEFAULT 0,
|
||||
device_limit INTEGER NOT NULL DEFAULT 0,
|
||||
quota INTEGER NOT NULL DEFAULT 0,
|
||||
new_user_only TINYINT DEFAULT 0,
|
||||
nodes VARCHAR(255),
|
||||
node_tags VARCHAR(255),
|
||||
show TINYINT NOT NULL DEFAULT 0,
|
||||
sell TINYINT NOT NULL DEFAULT 1,
|
||||
sort INTEGER NOT NULL DEFAULT 0,
|
||||
deduction_ratio INTEGER DEFAULT 0,
|
||||
allow_deduction TINYINT DEFAULT 1,
|
||||
reset_cycle INTEGER DEFAULT 0,
|
||||
renewal_reset TINYINT DEFAULT 0,
|
||||
show_original_price TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "order" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
parent_id INTEGER DEFAULT NULL,
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
subscription_user_id INTEGER NOT NULL DEFAULT 0,
|
||||
order_no VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,
|
||||
type TINYINT NOT NULL DEFAULT 1,
|
||||
quantity INTEGER NOT NULL DEFAULT 1,
|
||||
price INTEGER NOT NULL DEFAULT 0,
|
||||
amount INTEGER NOT NULL DEFAULT 0,
|
||||
gift_amount INTEGER NOT NULL DEFAULT 0,
|
||||
discount INTEGER NOT NULL DEFAULT 0,
|
||||
coupon VARCHAR(255) DEFAULT NULL,
|
||||
coupon_discount INTEGER NOT NULL DEFAULT 0,
|
||||
commission INTEGER NOT NULL DEFAULT 0,
|
||||
payment_id INTEGER NOT NULL DEFAULT 0,
|
||||
method VARCHAR(255) NOT NULL DEFAULT '',
|
||||
fee_amount INTEGER NOT NULL DEFAULT 0,
|
||||
trade_no VARCHAR(255) DEFAULT NULL,
|
||||
app_account_token VARCHAR(255) DEFAULT NULL,
|
||||
status TINYINT NOT NULL DEFAULT 1,
|
||||
subscribe_id INTEGER NOT NULL DEFAULT 0,
|
||||
subscribe_token VARCHAR(255) DEFAULT NULL,
|
||||
is_new TINYINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
deleted_at DATETIME DEFAULT NULL
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "user" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
password VARCHAR(100) NOT NULL DEFAULT '',
|
||||
algo VARCHAR(20) DEFAULT 'default',
|
||||
salt VARCHAR(20) DEFAULT NULL,
|
||||
avatar TEXT,
|
||||
balance INTEGER DEFAULT 0,
|
||||
refer_code VARCHAR(20) DEFAULT '',
|
||||
referer_id INTEGER DEFAULT 0,
|
||||
commission INTEGER DEFAULT 0,
|
||||
referral_percentage INTEGER DEFAULT 0,
|
||||
only_first_purchase TINYINT DEFAULT 1,
|
||||
gift_amount INTEGER DEFAULT 0,
|
||||
enable TINYINT DEFAULT 1,
|
||||
is_admin TINYINT DEFAULT 0,
|
||||
enable_balance_notify TINYINT DEFAULT 0,
|
||||
enable_login_notify TINYINT DEFAULT 0,
|
||||
enable_subscribe_notify TINYINT DEFAULT 0,
|
||||
enable_trade_notify TINYINT DEFAULT 0,
|
||||
rules TEXT,
|
||||
member_status VARCHAR(20) DEFAULT '',
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
deleted_at DATETIME DEFAULT NULL
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "payment" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
platform VARCHAR(100) NOT NULL DEFAULT '',
|
||||
icon VARCHAR(255) DEFAULT '',
|
||||
domain VARCHAR(255) DEFAULT '',
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
description TEXT,
|
||||
fee_mode TINYINT NOT NULL DEFAULT 0,
|
||||
fee_percent INTEGER DEFAULT 0,
|
||||
fee_amount INTEGER DEFAULT 0,
|
||||
enable TINYINT NOT NULL DEFAULT 1,
|
||||
token VARCHAR(255) NOT NULL DEFAULT '' UNIQUE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "user_subscribe" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
order_id INTEGER NOT NULL DEFAULT 0,
|
||||
subscribe_id INTEGER NOT NULL DEFAULT 0,
|
||||
start_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expire_time DATETIME DEFAULT NULL,
|
||||
finished_at DATETIME DEFAULT NULL,
|
||||
traffic INTEGER DEFAULT 0,
|
||||
download INTEGER DEFAULT 0,
|
||||
upload INTEGER DEFAULT 0,
|
||||
token VARCHAR(255) DEFAULT '' UNIQUE,
|
||||
uuid VARCHAR(255) DEFAULT '' UNIQUE,
|
||||
status TINYINT DEFAULT 0,
|
||||
note VARCHAR(500) DEFAULT '',
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "user_device" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip VARCHAR(255) NOT NULL DEFAULT '',
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
user_agent TEXT,
|
||||
identifier VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,
|
||||
short_code VARCHAR(255) NOT NULL DEFAULT '',
|
||||
online TINYINT NOT NULL DEFAULT 0,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "user_family" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_user_id INTEGER NOT NULL DEFAULT 0,
|
||||
max_members INTEGER NOT NULL DEFAULT 2,
|
||||
status TINYINT DEFAULT 0,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
deleted_at DATETIME DEFAULT NULL
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "user_family_member" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
family_id INTEGER NOT NULL DEFAULT 0,
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
role TINYINT DEFAULT 0,
|
||||
status TINYINT DEFAULT 0,
|
||||
join_source VARCHAR(32) NOT NULL DEFAULT '',
|
||||
joined_at DATETIME,
|
||||
left_at DATETIME DEFAULT NULL,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
deleted_at DATETIME DEFAULT NULL
|
||||
)`,
|
||||
}
|
||||
for _, sql := range sqls {
|
||||
require.NoError(t, db.Exec(sql).Error)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
// setupNewUserOnlyRedis 启动 miniredis,返回 redis.Client 和 miniredis 句柄
|
||||
func setupNewUserOnlyRedis(t *testing.T) (*redis.Client, *miniredis.Miniredis) {
|
||||
t.Helper()
|
||||
mr, err := miniredis.Run()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(mr.Close)
|
||||
rds := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||
return rds, mr
|
||||
}
|
||||
|
||||
// buildNewUserOnlySvcCtx 组装最小 ServiceContext(含 asynq Queue 使用 miniredis)
|
||||
func buildNewUserOnlySvcCtx(db *gorm.DB, rds *redis.Client, mr *miniredis.Miniredis) *svc.ServiceContext {
|
||||
queue := asynq.NewClient(asynq.RedisClientOpt{Addr: mr.Addr()})
|
||||
return &svc.ServiceContext{
|
||||
DB: db,
|
||||
Redis: rds,
|
||||
UserModel: user.NewModel(db, rds),
|
||||
OrderModel: modelOrder.NewModel(db, rds),
|
||||
SubscribeModel: subModel.NewModel(db, rds),
|
||||
PaymentModel: payment.NewModel(db, rds),
|
||||
Queue: queue,
|
||||
}
|
||||
}
|
||||
|
||||
// insertTestSubscribe 直接用 SQL 插入 subscribe 行(绕过 GORM hook 的 MySQL 方言)
|
||||
// new_user_only=true 时同时写入 discount JSON,使代码里的 discount 检查生效
|
||||
func insertTestSubscribe(t *testing.T, db *gorm.DB, id int64, newUserOnly bool) {
|
||||
t.Helper()
|
||||
nuOnly := 0
|
||||
discount := ""
|
||||
if newUserOnly {
|
||||
nuOnly = 1
|
||||
// discount JSON 包含一个 new_user_only=true 的 tier,匹配 quantity=1
|
||||
discount = `[{"quantity":1,"discount":90,"new_user_only":true}]`
|
||||
}
|
||||
err := db.Exec(`INSERT INTO "subscribe"
|
||||
(id, name, unit_price, inventory, sell, sort, new_user_only, discount, created_at, updated_at)
|
||||
VALUES (?, 'Test Plan', 1000, -1, 1, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||||
id, id, nuOnly, discount).Error
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// insertTestPayment 插入支付方式行
|
||||
func insertTestPayment(t *testing.T, db *gorm.DB, id int64) {
|
||||
t.Helper()
|
||||
err := db.Exec(`INSERT INTO "payment"
|
||||
(id, name, platform, config, enable, fee_mode, token)
|
||||
VALUES (?, 'Balance', 'balance', '{}', 1, 0, ?)`,
|
||||
id, "test-token").Error
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// insertTestUser 插入用户行,createdAt 可控
|
||||
func insertTestUser(t *testing.T, db *gorm.DB, id int64, createdAt time.Time) *user.User {
|
||||
t.Helper()
|
||||
err := db.Exec(`INSERT INTO "user"
|
||||
(id, password, balance, gift_amount, enable, created_at, updated_at)
|
||||
VALUES (?, '', 0, 0, 1, ?, datetime('now'))`,
|
||||
id, createdAt.UTC().Format("2006-01-02 15:04:05")).Error
|
||||
require.NoError(t, err)
|
||||
return &user.User{
|
||||
Id: id,
|
||||
GiftAmount: 0,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
func insertTestDevice(t *testing.T, db *gorm.DB, userID int64, identifier string, createdAt time.Time) {
|
||||
t.Helper()
|
||||
err := db.Exec(`INSERT INTO "user_device"
|
||||
(user_id, ip, user_agent, identifier, short_code, online, enabled, created_at, updated_at)
|
||||
VALUES (?, '127.0.0.1', 'test-agent', ?, '', 0, 1, ?, datetime('now'))`,
|
||||
userID,
|
||||
identifier,
|
||||
createdAt.UTC().Format("2006-01-02 15:04:05"),
|
||||
).Error
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func insertTestFamily(t *testing.T, db *gorm.DB, familyID, ownerUserID int64) {
|
||||
t.Helper()
|
||||
err := db.Exec(`INSERT INTO "user_family"
|
||||
(id, owner_user_id, max_members, status, created_at, updated_at)
|
||||
VALUES (?, ?, 3, 1, datetime('now'), datetime('now'))`,
|
||||
familyID,
|
||||
ownerUserID,
|
||||
).Error
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func insertTestFamilyMember(t *testing.T, db *gorm.DB, familyID, userID int64, role, status uint8, joinSource string) {
|
||||
t.Helper()
|
||||
err := db.Exec(`INSERT INTO "user_family_member"
|
||||
(family_id, user_id, role, status, join_source, joined_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'), datetime('now'))`,
|
||||
familyID,
|
||||
userID,
|
||||
role,
|
||||
status,
|
||||
joinSource,
|
||||
).Error
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// insertTestOrder 插入一条历史订单(status=2 表示已支付)
|
||||
func insertTestOrder(t *testing.T, db *gorm.DB, userID, subscribeID int64, status uint8) {
|
||||
t.Helper()
|
||||
err := db.Exec(`INSERT INTO "order"
|
||||
(user_id, order_no, type, status, subscribe_id, created_at, updated_at)
|
||||
VALUES (?, ?, 1, ?, ?, datetime('now'), datetime('now'))`,
|
||||
userID, "existing-order-no", status, subscribeID).Error
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func insertScopedTestOrder(t *testing.T, db *gorm.DB, orderNo string, userID, subscribeID int64, status uint8) {
|
||||
t.Helper()
|
||||
err := db.Exec(`INSERT INTO "order"
|
||||
(user_id, order_no, type, status, subscribe_id, created_at, updated_at)
|
||||
VALUES (?, ?, 1, ?, ?, datetime('now'), datetime('now'))`,
|
||||
userID, orderNo, status, subscribeID).Error
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// buildPurchaseCtx 把 user 放入 context(模拟中间件行为)
|
||||
func buildPurchaseCtx(u *user.User) context.Context {
|
||||
return context.WithValue(context.Background(), constant.CtxKeyUser, u)
|
||||
}
|
||||
|
||||
// TestPurchase_NewUserOnly_UserTooOld 验证:new_user_only=true,用户注册超过 24h → 返回 SubscribeNewUserOnly
|
||||
func TestPurchase_NewUserOnly_UserTooOld(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const subID = int64(1)
|
||||
const payID = int64(1)
|
||||
|
||||
insertTestSubscribe(t, db, subID, true) // new_user_only = true
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
// 用户注册 48 小时前 → 超出 24h 限制
|
||||
u := insertTestUser(t, db, 100, time.Now().Add(-48*time.Hour))
|
||||
ctx := buildPurchaseCtx(u)
|
||||
|
||||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||||
_, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
var errCode *xerr.CodeError
|
||||
require.ErrorAs(t, err, &errCode)
|
||||
assert.Equal(t, xerr.SubscribeNewUserOnly, errCode.GetErrCode(),
|
||||
"注册超过24h应返回 SubscribeNewUserOnly 错误码")
|
||||
|
||||
// 验证订单未被创建
|
||||
var count int64
|
||||
db.Model(&modelOrder.Order{}).Where("user_id = ?", u.Id).Count(&count)
|
||||
assert.Equal(t, int64(0), count, "用户注册超时,订单不应被创建")
|
||||
}
|
||||
|
||||
// TestPurchase_NewUserOnly_AlreadyPurchased 验证:new_user_only=true,用户是新用户但已购买过
|
||||
// → 允许下单(不拦截),但不享受新人折扣
|
||||
func TestPurchase_NewUserOnly_AlreadyPurchased(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const subID = int64(2)
|
||||
const payID = int64(1)
|
||||
|
||||
insertTestSubscribe(t, db, subID, true)
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
// 用户刚注册(2h前)→ 满足时间条件
|
||||
u := insertTestUser(t, db, 200, time.Now().Add(-2*time.Hour))
|
||||
|
||||
// 但已有一条 status=2 的历史订单(已支付)
|
||||
insertTestOrder(t, db, u.Id, subID, 2)
|
||||
|
||||
ctx := buildPurchaseCtx(u)
|
||||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
// 不应被拦截,允许下单
|
||||
require.NoError(t, err, "24h内已购用户应允许继续下单,不应返回错误")
|
||||
require.NotNil(t, resp)
|
||||
assert.NotEmpty(t, resp.OrderNo)
|
||||
|
||||
// 历史订单 +1(新增了一条)
|
||||
var count int64
|
||||
db.Model(&modelOrder.Order{}).Where("user_id = ?", u.Id).Count(&count)
|
||||
assert.Equal(t, int64(2), count, "应新增一条订单")
|
||||
|
||||
// 新订单无折扣:Amount=Price=1000
|
||||
var newOrder modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
|
||||
assert.Equal(t, int64(1000), newOrder.Amount, "已购用户不享受新人折扣,Amount 应等于 Price")
|
||||
assert.Equal(t, int64(0), newOrder.Discount, "Discount 应为 0")
|
||||
}
|
||||
|
||||
// TestPurchase_NewUserOnly_Success 验证:new_user_only=true,新用户首次购买 → 成功创建订单
|
||||
func TestPurchase_NewUserOnly_Success(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const subID = int64(3)
|
||||
const payID = int64(1)
|
||||
|
||||
insertTestSubscribe(t, db, subID, true)
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
// 用户 1 小时前注册(新用户),且没有历史订单
|
||||
u := insertTestUser(t, db, 300, time.Now().Add(-1*time.Hour))
|
||||
ctx := buildPurchaseCtx(u)
|
||||
|
||||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.NotEmpty(t, resp.OrderNo, "新用户首次购买应成功,返回订单号")
|
||||
|
||||
// 验证订单已写入数据库
|
||||
var o modelOrder.Order
|
||||
err = db.Where("order_no = ?", resp.OrderNo).First(&o).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, u.Id, o.UserId)
|
||||
assert.Equal(t, subID, o.SubscribeId)
|
||||
}
|
||||
|
||||
// TestPurchase_NewUserOnly_Disabled 验证:new_user_only=false 时,老用户也能正常购买
|
||||
func TestPurchase_NewUserOnly_Disabled(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const subID = int64(4)
|
||||
const payID = int64(1)
|
||||
|
||||
insertTestSubscribe(t, db, subID, false) // new_user_only = false
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
// 注册 30 天的老用户
|
||||
u := insertTestUser(t, db, 400, time.Now().Add(-30*24*time.Hour))
|
||||
ctx := buildPurchaseCtx(u)
|
||||
|
||||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.NotEmpty(t, resp.OrderNo, "new_user_only=false时老用户应能正常购买")
|
||||
}
|
||||
|
||||
// TestPurchase_SingleMode_PendingOldOrderCancelled 验证:单订阅模式下,已有 pending 订单时
|
||||
// 第二次下单应关闭旧单并创建新单(而非复用旧单)
|
||||
func TestPurchase_SingleMode_PendingOldOrderCancelled(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
svcCtx.Config.Subscribe.SingleModel = true
|
||||
|
||||
const subID = int64(5)
|
||||
const payID = int64(1)
|
||||
|
||||
insertTestSubscribe(t, db, subID, false)
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
u := insertTestUser(t, db, 500, time.Now().Add(-1*time.Hour))
|
||||
ctx := buildPurchaseCtx(u)
|
||||
|
||||
// 第一次下单(pending)
|
||||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||||
resp1, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp1)
|
||||
firstOrderNo := resp1.OrderNo
|
||||
assert.NotEmpty(t, firstOrderNo)
|
||||
|
||||
// 确认第一单 pending
|
||||
var firstOrder modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", firstOrderNo).First(&firstOrder).Error)
|
||||
assert.Equal(t, uint8(1), firstOrder.Status, "第一单应为 pending")
|
||||
|
||||
// 第二次下单(不同 quantity)
|
||||
logic2 := NewPurchaseLogic(ctx, svcCtx)
|
||||
resp2, err := logic2.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 3,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp2)
|
||||
secondOrderNo := resp2.OrderNo
|
||||
|
||||
// 新单与旧单不同
|
||||
assert.NotEqual(t, firstOrderNo, secondOrderNo, "第二次下单应创建新订单,不复用旧单")
|
||||
|
||||
// 旧单应被关闭(status=3)
|
||||
var closedOrder modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", firstOrderNo).First(&closedOrder).Error)
|
||||
assert.Equal(t, uint8(3), closedOrder.Status, "旧 pending 单应被关闭")
|
||||
|
||||
// 新单的 quantity 应为 3
|
||||
var newOrder modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", secondOrderNo).First(&newOrder).Error)
|
||||
assert.Equal(t, int64(3), newOrder.Quantity, "新单 quantity 应为 3")
|
||||
assert.Equal(t, uint8(1), newOrder.Status, "新单应为 pending 状态")
|
||||
}
|
||||
|
||||
// TestPurchase_SingleMode_NoPendingOrder 验证:单订阅模式下,没有旧 pending 单时正常创建
|
||||
func TestPurchase_SingleMode_NoPendingOrder(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
svcCtx.Config.Subscribe.SingleModel = true
|
||||
|
||||
const subID = int64(6)
|
||||
const payID = int64(1)
|
||||
|
||||
insertTestSubscribe(t, db, subID, false)
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
u := insertTestUser(t, db, 600, time.Now().Add(-1*time.Hour))
|
||||
ctx := buildPurchaseCtx(u)
|
||||
|
||||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.NotEmpty(t, resp.OrderNo, "无旧 pending 单时应正常创建新单")
|
||||
|
||||
var o modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&o).Error)
|
||||
assert.Equal(t, int64(2), o.Quantity)
|
||||
assert.Equal(t, uint8(1), o.Status)
|
||||
}
|
||||
|
||||
// TestPurchase_NewUserOnly_AlreadyPurchased_NoBlock 验证:new_user_only=true 套餐,
|
||||
// 24小时内但已购买过 → 允许下单,但不享受新人折扣(Discount=0,Amount=Price)
|
||||
func TestPurchase_NewUserOnly_AlreadyPurchased_NoBlock(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const subID = int64(7)
|
||||
const payID = int64(1)
|
||||
|
||||
// 套餐:unit_price=1000,discount=[{quantity:1,discount:80,new_user_only:true}]
|
||||
insertTestSubscribe(t, db, subID, true)
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
// 用户 1 小时前注册(新用户),但已有一条成功订单(status=2)
|
||||
u := insertTestUser(t, db, 700, time.Now().Add(-1*time.Hour))
|
||||
insertTestOrder(t, db, u.Id, subID, 2)
|
||||
|
||||
ctx := buildPurchaseCtx(u)
|
||||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
// 不应被拦截
|
||||
require.NoError(t, err, "24h内已购用户不应被拦截,应允许下单")
|
||||
require.NotNil(t, resp)
|
||||
assert.NotEmpty(t, resp.OrderNo)
|
||||
|
||||
// 验证订单金额:无折扣,Amount=Price=1000,Discount=0
|
||||
var newOrder modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
|
||||
assert.Equal(t, int64(1000), newOrder.Price, "Price 应为原价 1000")
|
||||
assert.Equal(t, int64(1000), newOrder.Amount, "已购用户不享受新人折扣,Amount 应等于 Price")
|
||||
assert.Equal(t, int64(0), newOrder.Discount, "Discount 应为 0")
|
||||
}
|
||||
|
||||
// TestPurchase_NewUserOnly_FirstPurchase_HasDiscount 验证:new_user_only=true 套餐,
|
||||
// 24小时内首次购买 → 允许下单且享受新人折扣
|
||||
func TestPurchase_NewUserOnly_FirstPurchase_HasDiscount(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const subID = int64(8)
|
||||
const payID = int64(1)
|
||||
|
||||
// 套餐:unit_price=1000,discount=[{quantity:1,discount:80,new_user_only:true}](8折)
|
||||
insertTestSubscribe(t, db, subID, true)
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
// 用户 1 小时前注册,无历史订单
|
||||
u := insertTestUser(t, db, 800, time.Now().Add(-1*time.Hour))
|
||||
ctx := buildPurchaseCtx(u)
|
||||
|
||||
logic := NewPurchaseLogic(ctx, svcCtx)
|
||||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
var newOrder modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
|
||||
assert.Equal(t, int64(1000), newOrder.Price, "Price 应为原价 1000")
|
||||
assert.Equal(t, int64(900), newOrder.Amount, "首次购买应享受9折,Amount=900")
|
||||
assert.Equal(t, int64(100), newOrder.Discount, "折扣金额应为 100")
|
||||
}
|
||||
|
||||
func TestPurchase_NewUserOnly_BindEmailScopeUsesEarliestDeviceTime(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const (
|
||||
subID = int64(9)
|
||||
payID = int64(1)
|
||||
ownerUserID = int64(901)
|
||||
memberUserID = int64(902)
|
||||
familyID = int64(99)
|
||||
)
|
||||
|
||||
insertTestSubscribe(t, db, subID, true)
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
|
||||
insertTestUser(t, db, memberUserID, time.Now().Add(-72*time.Hour))
|
||||
insertTestDevice(t, db, memberUserID, "device-eligibility-old", time.Now().Add(-72*time.Hour))
|
||||
insertTestFamily(t, db, familyID, ownerUserID)
|
||||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||||
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
|
||||
|
||||
logic := NewPurchaseLogic(buildPurchaseCtx(owner), svcCtx)
|
||||
_, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
var errCode *xerr.CodeError
|
||||
require.ErrorAs(t, err, &errCode)
|
||||
assert.Equal(t, xerr.SubscribeNewUserOnly, errCode.GetErrCode())
|
||||
}
|
||||
|
||||
func TestPurchase_NewUserOnly_BindEmailScopeSharesHistory(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const (
|
||||
subID = int64(10)
|
||||
payID = int64(1)
|
||||
ownerUserID = int64(1001)
|
||||
memberUserID = int64(1002)
|
||||
familyID = int64(109)
|
||||
)
|
||||
|
||||
insertTestSubscribe(t, db, subID, true)
|
||||
insertTestPayment(t, db, payID)
|
||||
|
||||
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
|
||||
insertTestUser(t, db, memberUserID, time.Now().Add(-2*time.Hour))
|
||||
insertTestDevice(t, db, memberUserID, "device-eligibility-shared", time.Now().Add(-2*time.Hour))
|
||||
insertTestFamily(t, db, familyID, ownerUserID)
|
||||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||||
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
|
||||
insertScopedTestOrder(t, db, "existing-scope-order", memberUserID, subID, 2)
|
||||
|
||||
logic := NewPurchaseLogic(buildPurchaseCtx(owner), svcCtx)
|
||||
resp, err := logic.Purchase(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Payment: payID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
var newOrder modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&newOrder).Error)
|
||||
assert.Equal(t, int64(1000), newOrder.Amount)
|
||||
assert.Equal(t, int64(0), newOrder.Discount)
|
||||
}
|
||||
|
||||
func ensureRenewalSubscribeColumns(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
for _, sql := range []string{
|
||||
`ALTER TABLE "user_subscribe" ADD COLUMN node_group_id INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE "user_subscribe" ADD COLUMN group_locked TINYINT NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE "user_subscribe" ADD COLUMN expired_download INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE "user_subscribe" ADD COLUMN expired_upload INTEGER NOT NULL DEFAULT 0`,
|
||||
} {
|
||||
require.NoError(t, db.Exec(sql).Error)
|
||||
}
|
||||
}
|
||||
|
||||
func insertRenewalUserSubscribe(t *testing.T, db *gorm.DB, id, userID, orderID, subscribeID int64, token, uuid string, start, expire time.Time) {
|
||||
t.Helper()
|
||||
require.NoError(t, db.Exec(`INSERT INTO "user_subscribe"
|
||||
(id, user_id, order_id, subscribe_id, start_time, expire_time, traffic, download, upload, token, uuid, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, 1, ?, ?)`,
|
||||
id,
|
||||
userID,
|
||||
orderID,
|
||||
subscribeID,
|
||||
start.UTC().Format("2006-01-02 15:04:05"),
|
||||
expire.UTC().Format("2006-01-02 15:04:05"),
|
||||
token,
|
||||
uuid,
|
||||
start.UTC().Format("2006-01-02 15:04:05"),
|
||||
time.Now().UTC().Format("2006-01-02 15:04:05"),
|
||||
).Error)
|
||||
}
|
||||
|
||||
func TestRenewalMemberRequestRedirectsToOwnerSubscribe(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
ensureRenewalSubscribeColumns(t, db)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const (
|
||||
planID = int64(1)
|
||||
paymentID = int64(2)
|
||||
ownerUserID = int64(29650)
|
||||
memberID = int64(20003)
|
||||
familyID = int64(88001)
|
||||
memberSubID = int64(10013)
|
||||
ownerSubID = int64(14074)
|
||||
)
|
||||
|
||||
insertTestSubscribe(t, db, planID, false)
|
||||
insertTestPayment(t, db, paymentID)
|
||||
member := insertTestUser(t, db, memberID, time.Now().Add(-24*time.Hour))
|
||||
insertTestUser(t, db, ownerUserID, time.Now().Add(-24*time.Hour))
|
||||
insertTestFamily(t, db, familyID, ownerUserID)
|
||||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||||
insertTestFamilyMember(t, db, familyID, memberID, user.FamilyRoleMember, user.FamilyMemberActive, "manual_invite")
|
||||
|
||||
insertRenewalUserSubscribe(t, db, memberSubID, memberID, 7448, planID, "member-token-10013", "member-uuid-10013",
|
||||
time.Date(2026, 4, 23, 19, 6, 40, 0, time.UTC),
|
||||
time.Date(2026, 4, 30, 19, 6, 40, 0, time.UTC),
|
||||
)
|
||||
insertRenewalUserSubscribe(t, db, ownerSubID, ownerUserID, 9999, planID, "owner-token-14074", "owner-uuid-14074",
|
||||
time.Date(2026, 4, 30, 13, 39, 33, 0, time.UTC),
|
||||
time.Date(2026, 5, 30, 16, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
resp, err := NewRenewalLogic(buildPurchaseCtx(member), svcCtx).Renewal(&types.RenewalOrderRequest{
|
||||
UserSubscribeID: memberSubID,
|
||||
Payment: paymentID,
|
||||
Quantity: 30,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
var created modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&created).Error)
|
||||
assert.Equal(t, memberID, created.UserId)
|
||||
assert.Equal(t, ownerUserID, created.SubscriptionUserId)
|
||||
assert.Equal(t, int64(9999), created.ParentId)
|
||||
assert.Equal(t, "owner-token-14074", created.SubscribeToken)
|
||||
}
|
||||
|
||||
func TestPreCreateOrder_NewUserOnly_BindEmailScopeUsesEarliestDeviceTime(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const (
|
||||
subID = int64(11)
|
||||
ownerUserID = int64(1101)
|
||||
memberUserID = int64(1102)
|
||||
familyID = int64(119)
|
||||
)
|
||||
|
||||
insertTestSubscribe(t, db, subID, true)
|
||||
|
||||
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
|
||||
insertTestUser(t, db, memberUserID, time.Now().Add(-96*time.Hour))
|
||||
insertTestDevice(t, db, memberUserID, "device-precreate-old", time.Now().Add(-96*time.Hour))
|
||||
insertTestFamily(t, db, familyID, ownerUserID)
|
||||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||||
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
|
||||
|
||||
logic := NewPreCreateOrderLogic(buildPurchaseCtx(owner), svcCtx)
|
||||
_, err := logic.PreCreateOrder(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
var errCode *xerr.CodeError
|
||||
require.ErrorAs(t, err, &errCode)
|
||||
assert.Equal(t, xerr.SubscribeNewUserOnly, errCode.GetErrCode())
|
||||
}
|
||||
|
||||
func TestPreCreateOrder_NewUserOnly_OrdinaryFamilyMemberDoesNotAffectEligibility(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const (
|
||||
subID = int64(12)
|
||||
ownerUserID = int64(1201)
|
||||
memberUserID = int64(1202)
|
||||
familyID = int64(129)
|
||||
)
|
||||
|
||||
insertTestSubscribe(t, db, subID, true)
|
||||
|
||||
owner := insertTestUser(t, db, ownerUserID, time.Now().Add(-1*time.Hour))
|
||||
insertTestUser(t, db, memberUserID, time.Now().Add(-96*time.Hour))
|
||||
insertTestDevice(t, db, memberUserID, "device-precreate-ordinary", time.Now().Add(-96*time.Hour))
|
||||
insertTestFamily(t, db, familyID, ownerUserID)
|
||||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||||
insertTestFamilyMember(t, db, familyID, memberUserID, user.FamilyRoleMember, user.FamilyMemberActive, "manual_invite")
|
||||
|
||||
logic := NewPreCreateOrderLogic(buildPurchaseCtx(owner), svcCtx)
|
||||
resp, err := logic.PreCreateOrder(&types.PurchaseOrderRequest{
|
||||
SubscribeId: subID,
|
||||
Quantity: 1,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, int64(900), resp.Amount)
|
||||
assert.Equal(t, int64(100), resp.Discount)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
modelOrder "github.com/perfect-panel/server/internal/model/order"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ensureRenewalInviteSubscribeColumns(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
for _, sql := range []string{
|
||||
`ALTER TABLE "user_subscribe" ADD COLUMN node_group_id INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE "user_subscribe" ADD COLUMN group_locked TINYINT NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE "user_subscribe" ADD COLUMN expired_download INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE "user_subscribe" ADD COLUMN expired_upload INTEGER NOT NULL DEFAULT 0`,
|
||||
} {
|
||||
require.NoError(t, db.Exec(sql).Error)
|
||||
}
|
||||
}
|
||||
|
||||
func insertRenewalInviteUserSubscribe(t *testing.T, db *gorm.DB, id, userID, orderID, subscribeID int64, token, uuid string, start, expire time.Time) {
|
||||
t.Helper()
|
||||
require.NoError(t, db.Exec(`INSERT INTO "user_subscribe"
|
||||
(id, user_id, order_id, subscribe_id, start_time, expire_time, traffic, download, upload, token, uuid, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, 1, ?, ?)`,
|
||||
id,
|
||||
userID,
|
||||
orderID,
|
||||
subscribeID,
|
||||
start.UTC().Format("2006-01-02 15:04:05"),
|
||||
expire.UTC().Format("2006-01-02 15:04:05"),
|
||||
token,
|
||||
uuid,
|
||||
start.UTC().Format("2006-01-02 15:04:05"),
|
||||
time.Now().UTC().Format("2006-01-02 15:04:05"),
|
||||
).Error)
|
||||
}
|
||||
|
||||
func TestRenewalFirstSuccessfulPaymentMarksOrderAsNew(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
ensureRenewalInviteSubscribeColumns(t, db)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const (
|
||||
planID = int64(1)
|
||||
paymentID = int64(2)
|
||||
ownerUserID = int64(31059)
|
||||
memberID = int64(31057)
|
||||
familyID = int64(5638)
|
||||
ownerSubID = int64(14672)
|
||||
)
|
||||
|
||||
insertTestSubscribe(t, db, planID, false)
|
||||
insertTestPayment(t, db, paymentID)
|
||||
member := insertTestUser(t, db, memberID, time.Now().Add(-2*time.Hour))
|
||||
insertTestUser(t, db, ownerUserID, time.Now().Add(-2*time.Hour))
|
||||
insertTestFamily(t, db, familyID, ownerUserID)
|
||||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||||
insertTestFamilyMember(t, db, familyID, memberID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
|
||||
|
||||
insertRenewalInviteUserSubscribe(t, db, ownerSubID, ownerUserID, 0, planID, "owner-trial-token", "owner-trial-uuid",
|
||||
time.Date(2026, 5, 2, 15, 53, 39, 0, time.UTC),
|
||||
time.Date(2026, 5, 9, 0, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
resp, err := NewRenewalLogic(buildPurchaseCtx(member), svcCtx).Renewal(&types.RenewalOrderRequest{
|
||||
UserSubscribeID: ownerSubID,
|
||||
Payment: paymentID,
|
||||
Quantity: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
var created modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&created).Error)
|
||||
require.Equal(t, uint8(2), created.Type)
|
||||
require.True(t, created.IsNew)
|
||||
require.Equal(t, memberID, created.UserId)
|
||||
require.Equal(t, ownerUserID, created.SubscriptionUserId)
|
||||
require.Equal(t, "owner-trial-token", created.SubscribeToken)
|
||||
}
|
||||
|
||||
func TestRenewalExistingSuccessfulPaymentMarksOrderAsNotNew(t *testing.T) {
|
||||
db := setupNewUserOnlyDB(t)
|
||||
ensureRenewalInviteSubscribeColumns(t, db)
|
||||
rds, mr := setupNewUserOnlyRedis(t)
|
||||
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||
|
||||
const (
|
||||
planID = int64(1)
|
||||
paymentID = int64(2)
|
||||
ownerUserID = int64(41059)
|
||||
memberID = int64(41057)
|
||||
familyID = int64(6638)
|
||||
ownerSubID = int64(24672)
|
||||
)
|
||||
|
||||
insertTestSubscribe(t, db, planID, false)
|
||||
insertTestPayment(t, db, paymentID)
|
||||
member := insertTestUser(t, db, memberID, time.Now().Add(-2*time.Hour))
|
||||
insertTestUser(t, db, ownerUserID, time.Now().Add(-2*time.Hour))
|
||||
insertTestFamily(t, db, familyID, ownerUserID)
|
||||
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||||
insertTestFamilyMember(t, db, familyID, memberID, user.FamilyRoleMember, user.FamilyMemberActive, "bind_email_with_verification")
|
||||
insertScopedTestOrder(t, db, "previous-successful-order", memberID, planID, 5)
|
||||
|
||||
insertRenewalInviteUserSubscribe(t, db, ownerSubID, ownerUserID, 0, planID, "owner-renewal-token", "owner-renewal-uuid",
|
||||
time.Date(2026, 5, 2, 15, 53, 39, 0, time.UTC),
|
||||
time.Date(2026, 5, 9, 0, 0, 0, 0, time.UTC),
|
||||
)
|
||||
|
||||
resp, err := NewRenewalLogic(buildPurchaseCtx(member), svcCtx).Renewal(&types.RenewalOrderRequest{
|
||||
UserSubscribeID: ownerSubID,
|
||||
Payment: paymentID,
|
||||
Quantity: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
var created modelOrder.Order
|
||||
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&created).Error)
|
||||
require.Equal(t, uint8(2), created.Type)
|
||||
require.False(t, created.IsNew)
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
modelOrder "github.com/perfect-panel/server/internal/model/order"
|
||||
modelUser "github.com/perfect-panel/server/internal/model/user"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func newFamilyBindingTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite db: %v", err)
|
||||
}
|
||||
if err = db.Exec(`
|
||||
CREATE TABLE user_subscribe (
|
||||
id integer primary key,
|
||||
user_id integer not null,
|
||||
order_id integer not null,
|
||||
subscribe_id integer not null,
|
||||
start_time datetime,
|
||||
expire_time datetime,
|
||||
token text,
|
||||
uuid text,
|
||||
status integer,
|
||||
created_at datetime,
|
||||
updated_at datetime
|
||||
)`).Error; err != nil {
|
||||
t.Fatalf("create user_subscribe schema: %v", err)
|
||||
}
|
||||
if err = db.Exec(`
|
||||
CREATE TABLE "order" (
|
||||
id integer primary key,
|
||||
user_id integer not null,
|
||||
subscription_user_id integer not null,
|
||||
order_no text,
|
||||
subscribe_token text,
|
||||
status integer,
|
||||
subscribe_id integer,
|
||||
created_at datetime,
|
||||
updated_at datetime
|
||||
)`).Error; err != nil {
|
||||
t.Fatalf("create order schema: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func insertFamilyBindingTestOrder(t *testing.T, db *gorm.DB, order modelOrder.Order) {
|
||||
t.Helper()
|
||||
|
||||
if err := db.Exec(`
|
||||
INSERT INTO "order" (
|
||||
id, user_id, subscription_user_id, order_no, subscribe_token, status, subscribe_id, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
order.Id,
|
||||
order.UserId,
|
||||
order.SubscriptionUserId,
|
||||
order.OrderNo,
|
||||
order.SubscribeToken,
|
||||
order.Status,
|
||||
order.SubscribeId,
|
||||
order.CreatedAt,
|
||||
order.UpdatedAt,
|
||||
).Error; err != nil {
|
||||
t.Fatalf("insert order %d: %v", order.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
func insertFamilyBindingTestSubscribe(t *testing.T, db *gorm.DB, sub modelUser.Subscribe) {
|
||||
t.Helper()
|
||||
|
||||
if err := db.Exec(`
|
||||
INSERT INTO user_subscribe (
|
||||
id, user_id, order_id, subscribe_id, start_time, expire_time, token, uuid, status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
sub.Id,
|
||||
sub.UserId,
|
||||
sub.OrderId,
|
||||
sub.SubscribeId,
|
||||
sub.StartTime,
|
||||
sub.ExpireTime,
|
||||
sub.Token,
|
||||
sub.UUID,
|
||||
sub.Status,
|
||||
sub.CreatedAt,
|
||||
sub.UpdatedAt,
|
||||
).Error; err != nil {
|
||||
t.Fatalf("insert subscribe %d: %v", sub.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveMemberSubscribesToOwnerMovesSubscribeAndCurrentOrder(t *testing.T) {
|
||||
db := newFamilyBindingTestDB(t)
|
||||
memberUserID := int64(1001)
|
||||
ownerUserID := int64(2001)
|
||||
now := time.Now().Add(-time.Hour)
|
||||
expireAt := now.Add(30 * 24 * time.Hour)
|
||||
|
||||
currentOrder := modelOrder.Order{
|
||||
Id: 9001,
|
||||
UserId: memberUserID,
|
||||
SubscriptionUserId: memberUserID,
|
||||
OrderNo: "order-current",
|
||||
SubscribeToken: "token-current",
|
||||
Status: 5,
|
||||
SubscribeId: 3001,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
unlinkedOrder := modelOrder.Order{
|
||||
Id: 9002,
|
||||
UserId: memberUserID,
|
||||
SubscriptionUserId: memberUserID,
|
||||
OrderNo: "order-unlinked",
|
||||
SubscribeToken: "token-unlinked",
|
||||
Status: 5,
|
||||
SubscribeId: 3001,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
sub := modelUser.Subscribe{
|
||||
Id: 7001,
|
||||
UserId: memberUserID,
|
||||
OrderId: currentOrder.Id,
|
||||
SubscribeId: currentOrder.SubscribeId,
|
||||
StartTime: now,
|
||||
ExpireTime: expireAt,
|
||||
Token: "token-current",
|
||||
UUID: "uuid-current",
|
||||
Status: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
insertFamilyBindingTestOrder(t, db, currentOrder)
|
||||
insertFamilyBindingTestOrder(t, db, unlinkedOrder)
|
||||
insertFamilyBindingTestSubscribe(t, db, sub)
|
||||
|
||||
var moved []modelUser.Subscribe
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
var err error
|
||||
moved, err = moveMemberSubscribesToOwner(tx, memberUserID, ownerUserID)
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("move member subscribes: %v", err)
|
||||
}
|
||||
|
||||
if len(moved) != 1 {
|
||||
t.Fatalf("moved subscribes length = %d, want 1", len(moved))
|
||||
}
|
||||
if moved[0].UserId != memberUserID {
|
||||
t.Fatalf("moved cache copy user_id = %d, want original member %d", moved[0].UserId, memberUserID)
|
||||
}
|
||||
|
||||
var gotSub modelUser.Subscribe
|
||||
if err := db.First(&gotSub, "id = ?", sub.Id).Error; err != nil {
|
||||
t.Fatalf("query moved subscribe: %v", err)
|
||||
}
|
||||
if gotSub.UserId != ownerUserID {
|
||||
t.Fatalf("subscribe user_id = %d, want owner %d", gotSub.UserId, ownerUserID)
|
||||
}
|
||||
if gotSub.Token == "" || gotSub.Token == sub.Token {
|
||||
t.Fatalf("subscribe token = %q, want regenerated from old token %q", gotSub.Token, sub.Token)
|
||||
}
|
||||
if gotSub.UUID == "" || gotSub.UUID == sub.UUID {
|
||||
t.Fatalf("subscribe uuid = %q, want regenerated from old uuid %q", gotSub.UUID, sub.UUID)
|
||||
}
|
||||
|
||||
var gotOrder modelOrder.Order
|
||||
if err := db.First(&gotOrder, "id = ?", currentOrder.Id).Error; err != nil {
|
||||
t.Fatalf("query updated order: %v", err)
|
||||
}
|
||||
if gotOrder.SubscriptionUserId != ownerUserID {
|
||||
t.Fatalf("current order subscription_user_id = %d, want owner %d", gotOrder.SubscriptionUserId, ownerUserID)
|
||||
}
|
||||
if gotOrder.SubscribeToken != gotSub.Token {
|
||||
t.Fatalf("current order subscribe_token = %q, want regenerated subscribe token %q", gotOrder.SubscribeToken, gotSub.Token)
|
||||
}
|
||||
|
||||
var gotUnlinkedOrder modelOrder.Order
|
||||
if err := db.First(&gotUnlinkedOrder, "id = ?", unlinkedOrder.Id).Error; err != nil {
|
||||
t.Fatalf("query unlinked order: %v", err)
|
||||
}
|
||||
if gotUnlinkedOrder.SubscriptionUserId != memberUserID {
|
||||
t.Fatalf("unlinked order subscription_user_id = %d, want unchanged member %d", gotUnlinkedOrder.SubscriptionUserId, memberUserID)
|
||||
}
|
||||
if gotUnlinkedOrder.SubscribeToken != unlinkedOrder.SubscribeToken {
|
||||
t.Fatalf("unlinked order subscribe_token = %q, want unchanged token %q", gotUnlinkedOrder.SubscribeToken, unlinkedOrder.SubscribeToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveMemberSubscribesToOwnerNoSubscribesIsNoop(t *testing.T) {
|
||||
db := newFamilyBindingTestDB(t)
|
||||
|
||||
var moved []modelUser.Subscribe
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
var err error
|
||||
moved, err = moveMemberSubscribesToOwner(tx, 1001, 2001)
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("move empty member subscribes: %v", err)
|
||||
}
|
||||
if len(moved) != 0 {
|
||||
t.Fatalf("moved subscribes length = %d, want 0", len(moved))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferMemberSubscribesToOwnerStillDiscardsMemberSubscribes(t *testing.T) {
|
||||
db := newFamilyBindingTestDB(t)
|
||||
memberUserID := int64(1001)
|
||||
ownerUserID := int64(2001)
|
||||
now := time.Now().Add(-time.Hour)
|
||||
|
||||
order := modelOrder.Order{
|
||||
Id: 9001,
|
||||
UserId: memberUserID,
|
||||
SubscriptionUserId: memberUserID,
|
||||
OrderNo: "order-discard",
|
||||
SubscribeToken: "token-discard",
|
||||
Status: 5,
|
||||
SubscribeId: 3001,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
sub := modelUser.Subscribe{
|
||||
Id: 7001,
|
||||
UserId: memberUserID,
|
||||
OrderId: order.Id,
|
||||
SubscribeId: order.SubscribeId,
|
||||
StartTime: now,
|
||||
ExpireTime: now.Add(30 * 24 * time.Hour),
|
||||
Token: "token-discard",
|
||||
UUID: "uuid-discard",
|
||||
Status: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
insertFamilyBindingTestOrder(t, db, order)
|
||||
insertFamilyBindingTestSubscribe(t, db, sub)
|
||||
|
||||
var removed []modelUser.Subscribe
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
var err error
|
||||
removed, err = transferMemberSubscribesToOwner(tx, memberUserID, ownerUserID)
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("discard member subscribes: %v", err)
|
||||
}
|
||||
|
||||
if len(removed) != 1 {
|
||||
t.Fatalf("removed subscribes length = %d, want 1", len(removed))
|
||||
}
|
||||
if removed[0].UserId != memberUserID {
|
||||
t.Fatalf("removed cache copy user_id = %d, want original member %d", removed[0].UserId, memberUserID)
|
||||
}
|
||||
|
||||
var memberCount int64
|
||||
if err := db.Model(&modelUser.Subscribe{}).Where("user_id = ?", memberUserID).Count(&memberCount).Error; err != nil {
|
||||
t.Fatalf("count member subscribes: %v", err)
|
||||
}
|
||||
if memberCount != 0 {
|
||||
t.Fatalf("member subscribe count = %d, want 0", memberCount)
|
||||
}
|
||||
|
||||
var ownerCount int64
|
||||
if err := db.Model(&modelUser.Subscribe{}).Where("user_id = ?", ownerUserID).Count(&ownerCount).Error; err != nil {
|
||||
t.Fatalf("count owner subscribes: %v", err)
|
||||
}
|
||||
if ownerCount != 0 {
|
||||
t.Fatalf("owner subscribe count = %d, want 0", ownerCount)
|
||||
}
|
||||
|
||||
var gotOrder modelOrder.Order
|
||||
if err := db.First(&gotOrder, "id = ?", order.Id).Error; err != nil {
|
||||
t.Fatalf("query order: %v", err)
|
||||
}
|
||||
if gotOrder.SubscriptionUserId != memberUserID {
|
||||
t.Fatalf("discard path order subscription_user_id = %d, want unchanged member %d", gotOrder.SubscriptionUserId, memberUserID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user