Files
wangjia b5ab92a57e
ci / server (push) Failing after 13s
ci / design-tokens (push) Failing after 11s
fix: 应用 xhigh 代码评审的跨端修复
来自 xhigh code review 的正确性/健壮性修复,覆盖全部五端:
- server:鉴权 fail-closed、计量交叉校验与配额扣穿处理、WS 网关并发与关闭顺序、
  billing 行锁、redis Lua 过期与设备槽刷新、config 解析
- desktop:会话 epoch 防串话、WS 重连与 401 处理、api 客户端复用、统一 usePoll 轮询
- android:握手时序、请求头封装、账户状态派生、按需重组
- ios:finalize 宽限、串行采集、错误文案服务端优先、删除死代码 CommitController

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:50:08 +08:00

106 lines
3.1 KiB
Go
Raw Permalink 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"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
)
func rdb(t *testing.T) *redis.Client {
t.Helper()
mr := miniredis.RunT(t)
return redis.NewClient(&redis.Options{Addr: mr.Addr()})
}
func TestAllowSessionWindow(t *testing.T) {
r := rdb(t)
ctx := context.Background()
now := time.Now()
// 30 次放行,第 31 次拒绝
for i := 0; i < 30; i++ {
ok, err := AllowSession(ctx, r, "dev1", now)
if err != nil || !ok {
t.Fatalf("session %d should pass: ok=%v err=%v", i+1, ok, err)
}
}
if ok, _ := AllowSession(ctx, r, "dev1", now); ok {
t.Fatal("31st session should be rejected")
}
// 其他设备不受影响
if ok, _ := AllowSession(ctx, r, "dev2", now); !ok {
t.Fatal("other device should pass")
}
}
// TestAudioWindowExhausted 复用生产路径(AudioWindowExhausted 只查 + RecordAudioSeconds 记账)。
func TestAudioWindowExhausted(t *testing.T) {
r := rdb(t)
ctx := context.Background()
now := time.Now()
// 初始未满
if full, _ := AudioWindowExhausted(ctx, r, "dev1", now); full {
t.Fatal("fresh window should not be exhausted")
}
// 记满后窗口耗尽(AudioWindowExhausted 只查不记,val=0sum>limit 才算满)
if err := RecordAudioSeconds(ctx, r, "dev1", 1801, now); err != nil {
t.Fatal(err)
}
if full, _ := AudioWindowExhausted(ctx, r, "dev1", now.Add(time.Second)); !full {
t.Fatal("window should be exhausted after exceeding 1800s")
}
// 其他设备不受影响
if full, _ := AudioWindowExhausted(ctx, r, "dev2", now); full {
t.Fatal("other device window should not be exhausted")
}
}
func TestDeviceSlot(t *testing.T) {
r := rdb(t)
ctx := context.Background()
ok, err := AcquireDeviceSlot(ctx, r, "dev1", "s1")
if err != nil || !ok {
t.Fatalf("first acquire should pass: %v", err)
}
if ok, _ := AcquireDeviceSlot(ctx, r, "dev1", "s2"); ok {
t.Fatal("second concurrent acquire should fail")
}
// 非持有者释放无效
if err := ReleaseDeviceSlot(ctx, r, "dev1", "s2"); err != nil {
t.Fatal(err)
}
if ok, _ := AcquireDeviceSlot(ctx, r, "dev1", "s3"); ok {
t.Fatal("slot should still be held by s1")
}
// 持有者释放后可再获取
if err := ReleaseDeviceSlot(ctx, r, "dev1", "s1"); err != nil {
t.Fatal(err)
}
if ok, _ := AcquireDeviceSlot(ctx, r, "dev1", "s3"); !ok {
t.Fatal("acquire after release should pass")
}
}
// TestRefreshDeviceSlot 仅持有者能续期;非持有者续期无效(17D)。
func TestRefreshDeviceSlot(t *testing.T) {
r := rdb(t)
ctx := context.Background()
if ok, err := AcquireDeviceSlot(ctx, r, "dev1", "s1"); err != nil || !ok {
t.Fatalf("acquire should pass: %v", err)
}
// 持有者续期成功
if ok, err := RefreshDeviceSlot(ctx, r, "dev1", "s1"); err != nil || !ok {
t.Fatalf("holder refresh should succeed: ok=%v err=%v", ok, err)
}
// 非持有者续期失败
if ok, _ := RefreshDeviceSlot(ctx, r, "dev1", "s2"); ok {
t.Fatal("non-holder refresh should fail")
}
// 续期未释放槽位:他人仍抢不到
if ok, _ := AcquireDeviceSlot(ctx, r, "dev1", "s2"); ok {
t.Fatal("slot should still be held after refresh")
}
}