- 3.1 bcrypt 移出事务 (Register): repository.HashPassword 前移到 db.Transaction 之前,消除连接池占用。
- 3.2 Login 消除用户枚举 + 限流 + timing 抹平: pkg/errors 加 ErrInvalidCredential
/ErrTooManyLoginAttempts; 用户不存在/密码错/密码空 三路径统一返回同一错误;
mobile 5次/ip 20次 per 15min 限流 (Redis, fail-open 降级); user-not-found 走
dummy bcrypt 抹平 ~100ms 时序差,完全消除枚举侧信道;空密码分支已核实无时序 leak。
- 3.3 MQ streams adapter 停用 → stub: 0 业务调用方, 新 stub EventProducer.Publish no-op;
pkg/mq/mq.go Init 不再装配 streams; 全仓 grep 验证 11 处硬编码
'gallery'/'default' 集中到 pkg/queue/consts (值不变, 仅消漂移)。
- 3.5 JWT 密钥治理: pkg/jwt MustInit fail-fast + atomic.Value (见上一个 commit 293c7b1)。
- 3.6 aiChat 健壮性: SaveContext 用 persona.ID(非 req.PersonaId); Redis/memory 错误
记 WARN 不静默; Dify err 映射稳定用户文案,原始 err 仅服务端日志。
- 3.7 statistic.Client 重构: TrackEvent 改 buffered channel (cap 1024) + dispatchLoop
worker; 失败 ERROR 日志带字段; drop 记 WARN; Close 可重复调用。
- 3.8 网关聚合: StarCache (60s TTL, singleflight) 替换 5+ 处 GetFanIdentities 链式调用;
DeleteAccount 改网关直调 userService.DeleteAccount(避免改 hand-written triple.go
风险,见报告 §5 proto 风险复盘); 铸造双写改异步 channel+consumer (3 retry)。
- 大量单测: 各子项 TDD (RED→GREEN), 关键并发 race_test (50 goroutine)。
Co-Authored-By: Claude <noreply@anthropic.com>
140 lines
3.3 KiB
Go
140 lines
3.3 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestMintSuccessChannel_BufferAndOverflow 验证 mintSuccessCh 的 256 cap + 非阻塞投递行为。
|
|
// 真实消费路径涉及 DB,这里只验证 channel 行为:
|
|
// 1) 256 buffer 容量满足
|
|
// 2) 256 cap 满后再投递走 default 分支(不阻塞)
|
|
// 3) 投递出的事件能被 consumer 协程读取
|
|
func TestMintSuccessChannel_BufferAndOverflow(t *testing.T) {
|
|
ctrl := &AssetController{
|
|
mintSuccessCh: make(chan *MintSuccessEvent, 256),
|
|
}
|
|
|
|
// 1. 投递 256 个事件,全部应入队(channel 容量刚好)
|
|
for i := 0; i < 256; i++ {
|
|
ev := &MintSuccessEvent{
|
|
UserID: int64(i),
|
|
AssetID: int64(1000 + i),
|
|
OrderID: "order",
|
|
InstanceNo: "inst",
|
|
}
|
|
select {
|
|
case ctrl.mintSuccessCh <- ev:
|
|
// 期望入队
|
|
default:
|
|
t.Fatalf("event %d should be enqueued, channel not full yet", i)
|
|
}
|
|
}
|
|
|
|
// 2. 现在 channel 满,再投递应走 default(非阻塞丢弃)
|
|
dropped := false
|
|
extra := &MintSuccessEvent{UserID: 999, AssetID: 9999, OrderID: "x", InstanceNo: "y"}
|
|
select {
|
|
case ctrl.mintSuccessCh <- extra:
|
|
// 不应到这里
|
|
default:
|
|
dropped = true
|
|
}
|
|
if !dropped {
|
|
t.Fatal("expected extra event to be dropped when channel full")
|
|
}
|
|
|
|
// 3. 启 consumer 协程消费
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
// 简单 consume:读到 256 个就停
|
|
consumed := 0
|
|
for consumed < 256 {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case ev := <-ctrl.mintSuccessCh:
|
|
_ = ev
|
|
consumed++
|
|
}
|
|
}
|
|
}()
|
|
|
|
// 4. 等待 consumer 读完
|
|
done := make(chan struct{})
|
|
go func() {
|
|
wg.Wait()
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
// OK
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("consumer did not drain 256 events within 2s")
|
|
}
|
|
|
|
// 5. 再次投递,通道已空,应能入队
|
|
for i := 0; i < 256; i++ {
|
|
ev := &MintSuccessEvent{UserID: int64(i), AssetID: int64(i)}
|
|
select {
|
|
case ctrl.mintSuccessCh <- ev:
|
|
default:
|
|
t.Fatalf("post-drain event %d should be enqueued", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMintSuccessChannel_NonBlockingSend 验证正常路径上 Send 不阻塞(空 channel 立即入队)。
|
|
func TestMintSuccessChannel_NonBlockingSend(t *testing.T) {
|
|
ctrl := &AssetController{
|
|
mintSuccessCh: make(chan *MintSuccessEvent, 256),
|
|
}
|
|
|
|
ev := &MintSuccessEvent{
|
|
UserID: 42,
|
|
AssetID: 100,
|
|
OrderID: "order-abc",
|
|
InstanceNo: "inst-xyz",
|
|
}
|
|
|
|
// 空 channel:Send 立即返回
|
|
done := make(chan struct{})
|
|
go func() {
|
|
ctrl.mintSuccessCh <- ev
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
// OK
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("send to empty channel should not block")
|
|
}
|
|
|
|
// 验证可读出
|
|
got := <-ctrl.mintSuccessCh
|
|
if got.UserID != 42 || got.AssetID != 100 || got.OrderID != "order-abc" {
|
|
t.Fatalf("got event payload mismatch: %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestMintSuccessEvent_StructFields 验证 MintSuccessEvent payload 字段能够在测试中正确传递,
|
|
// 替代直接构造 resp.GetAsset() 的复杂 setup。
|
|
func TestMintSuccessEvent_StructFields(t *testing.T) {
|
|
ev := &MintSuccessEvent{
|
|
UserID: 1,
|
|
AssetID: 2,
|
|
OrderID: "order-3",
|
|
InstanceNo: "inst-4",
|
|
}
|
|
if ev.UserID != 1 || ev.AssetID != 2 || ev.OrderID != "order-3" || ev.InstanceNo != "inst-4" {
|
|
t.Fatal("MintSuccessEvent field round-trip failed")
|
|
}
|
|
}
|