Files
sundynix-agentix/sundynix-gateway/internal/store/completeness_fixes_test.go
T
Blizzard ce7cca657e fix(backend): 补齐三处漏洞——暂停租户拦截/金额不符落审计终态/邀请码列表滤失效
P0 暂停租户是空开关:admin 能设 suspended,但 preflight 只查预算+余额、不看
租户 status → 暂停后照样能提交烧积分。加 TenantSuspended 校验(活跃租户 + 分叉时
的计费租户都拦),403 拒绝。

P1 金额不符只刷日志:回调/查单判了不符却没落审计、订单永远卡 pending 被补偿定时器
每轮重扫刷屏。加 disputed 终态 + MarkOrderDisputed(CAS 只挂一次) + 审计(首次写一次);
disputed 不在 pending 扫描内,停止无限重扫。admin /orders?status=disputed 可查。

P1 邀请码列表混入失效码:ListInvites 只按 status=active 过滤,过期/满员的码仍显示为
有效、误导邀请人。有效列表加 expires_at>now 且 used<max 过滤(RedeemInvite 本就会拒,
这里修的是展示一致性)。

三处均带 store 单测(TenantSuspended/MarkOrderDisputed CAS/ListInvites 过滤)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 14:20:34 +08:00

85 lines
2.8 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 store
import (
"context"
"testing"
"time"
)
// 暂停租户是个真管控,不能是空开关:TenantSuspended 必须如实反映 status。
func TestTenantSuspended(t *testing.T) {
p := newTestStore(t)
ctx := context.Background()
seedTenant(t, p, "t1") // seedTenant 建的是 active
if p.TenantSuspended(ctx, "t1") {
t.Fatal("active 租户不该报暂停")
}
if err := p.SetTenantStatus(ctx, "t1", "suspended"); err != nil {
t.Fatalf("置暂停失败: %v", err)
}
if !p.TenantSuspended(ctx, "t1") {
t.Fatal("suspended 租户应报暂停")
}
// 查不到的租户按未暂停处理(宁放行不误封)。
if p.TenantSuspended(ctx, "nope") {
t.Fatal("不存在的租户不该报暂停")
}
}
// 金额不符挂起:CAS 只挂一次,且不动已 paid 单。
func TestMarkOrderDisputed_CAS(t *testing.T) {
p := newTestStore(t)
ctx := context.Background()
seedTenant(t, p, "t1")
o := &PaymentOrder{TenantID: "t1", UserID: "u1", AmountFen: 990, Channel: ChannelWechat, Status: OrderPending}
if err := p.CreateOrder(ctx, o); err != nil {
t.Fatalf("建单失败: %v", err)
}
changed, err := p.MarkOrderDisputed(ctx, o.ID)
if err != nil || !changed {
t.Fatalf("首次挂起应 changed=true: %v %v", changed, err)
}
again, _ := p.MarkOrderDisputed(ctx, o.ID)
if again {
t.Fatal("重复挂起应 changed=false(审计只写一次)")
}
got, _ := p.GetOrder(ctx, o.ID)
if got.Status != OrderDisputed {
t.Fatalf("状态应 disputed,得 %q", got.Status)
}
// 已 paid 的单不能被挂起(CAS 只认 pending)。
paid := &PaymentOrder{TenantID: "t1", UserID: "u1", AmountFen: 990, Channel: ChannelWechat, Status: OrderPaid}
p.CreateOrder(ctx, paid)
if c, _ := p.MarkOrderDisputed(ctx, paid.ID); c {
t.Fatal("已 paid 单不该能挂起")
}
}
// 有效邀请码列表要滤掉过期/满员,否则误导邀请人。
func TestListInvites_FiltersDeadCodes(t *testing.T) {
p := newTestStore(t)
ctx := context.Background()
seedTenant(t, p, "t1")
good, _ := p.CreateInvite(ctx, "t1", "inv", RoleMember, time.Now().Add(time.Hour), 0)
expired, _ := p.CreateInvite(ctx, "t1", "inv", RoleMember, time.Now().Add(-time.Hour), 0)
full, _ := p.CreateInvite(ctx, "t1", "inv", RoleMember, time.Now().Add(time.Hour), 1)
// 把 full 灌满
p.db.Model(&TenantInvite{}).Where("id = ?", full.ID).UpdateColumn("used_count", 1)
active := p.ListInvites(ctx, "t1", true)
if len(active) != 1 || active[0].ID != good.ID {
ids := make([]string, len(active))
for i, a := range active {
ids[i] = a.ID
}
t.Fatalf("有效列表应只剩 good%s),得 %v", good.ID, ids)
}
_ = expired
// onlyActive=false 仍应看到全部三条(管理/审计用途)。
if all := p.ListInvites(ctx, "t1", false); len(all) != 3 {
t.Fatalf("全量列表应 3 条,得 %d", len(all))
}
}