Files
sundynix-agentix/sundynix-gateway/internal/store/completeness_fixes_test.go
T
Blizzard ac38d5e663 fix(prod): 后端生产级 A 类硬伤全清(7 项:授权/崩溃点/全表扫/限流/鉴权/限额)
部署前生产级审计(可靠性/数据层/安全三路)后,清掉 7 处代码级硬伤:

A1 后台定时器 goroutine 无 panic recover → 单个 DB panic 崩整个 gateway。加 safeGo/
   safeCall,包住订阅/掉单补偿/微信推送/探针 goroutine,单轮 tick 再兜一层。
A2 提示词控制面(建/激活/停用,热广播全服务)只 RequireAuth → 任意登录用户改全局提示词。
   三写端点+列表挂 RequireAdmin。
A3 HITL 审批端点无角色门 → viewer 可放行烧钱执行。加 RequireTenantRole(member)。
A4 审计/护栏列表 limit 无校验,limit=-1 让 gorm 取消 LIMIT 全表扫。加 clampLimit/
   clampOffset,AdminTasks/AdminSpaces 补上界。
A5 限流 Redis 一挂就完全放行(fail-open)。加进程内固定窗口兜底(fail-safe) + 登录/注册
   按 IP 专用严限流(10/min)。
A6 公开 by-id 端点(stream/exec/report导出/kb导入流)无鉴权无租户过滤。加
   AuthFromHeaderOrQuery(从 ?token= 取 JWT) + task/report 按 owner 归属校验;桌面端
   5 处 EventSource/下载 URL 经 tokenQuery 附 JWT。
A7 文件上传无大小上限(整文件进内存 OOM 面) → 50MB 闸(KB_MAX_UPLOAD_BYTES)+ LimitReader;
   http.Server 加 ReadHeaderTimeout/ReadTimeout/MaxHeaderBytes(不设 WriteTimeout 保 SSE)。

带单测:clampLimit/safeCall/procLimiter/AuthFromHeaderOrQuery/TaskOwner。
build+vet+全量 test 绿;desktop tsc 绿。B(迁移工具/实时探针/出网韧性/登录锁定/leader选举)
与 C(TLS/PG HA/K8s/备份自动化/可观测)分期后做,参照 production_readiness.md。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:04:47 +08:00

101 lines
3.3 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))
}
}
// A6TaskOwner 供公开 by-id 端点判权——按 task_id 返回提交者,跨租户可查,不存在返回空。
func TestTaskOwner(t *testing.T) {
p := newTestStore(t)
seedTenant(t, p, "t1")
ctx := context.Background()
if err := p.SaveTask(ctx, "user-1", "task-abc", "{}"); err != nil {
t.Fatalf("建任务失败: %v", err)
}
if got := p.TaskOwner(ctx, "task-abc"); got != "user-1" {
t.Fatalf("owner 应 user-1,得 %q", got)
}
if got := p.TaskOwner(ctx, "nope"); got != "" {
t.Fatalf("不存在任务应返回空,得 %q", got)
}
}