fix: 审查修复波次(P0x4 + P1x30+)
- domain/rules: 计分与排名解耦、破纪录按组别、tieBreak 生效、调整分不丢、移除 aggregate 假能力 - realtime: 新建 internal/dto,快照三项契约对齐,/ws 只认会话身份 - httpapi: 流水幂等、standings 整表载荷、广播接线、强制改密、XFF 防伪造、records 写入、审计、赛事包内容、发布前备份、result.points、实例列表 - web: FormData 上传、YAML 解析、快照归一化、大屏解析层、bootstrap 降级、访客判定、主题切换 - console: 优雅关闭握手、中文化、免验证改管理员账号、选屏与日志轮转修复 - docs: 用户文档三份 + README - 主代理: 破纪录加分接线(levels 未加载导致恒 0)
This commit is contained in:
1 parent
8dae63ee70
commit
7f9701c465
61 files changed
+3844
-333
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -17,6 +18,8 @@ import (
|
||||
"github.com/coder/websocket"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/auth"
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/middleware"
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/store"
|
||||
)
|
||||
|
||||
@@ -28,12 +31,22 @@ type testEnv struct {
|
||||
srv *httptest.Server
|
||||
}
|
||||
|
||||
// newTestEnv 建一个未登录(guest)的服务端:默认身份与大屏端一致。
|
||||
func newTestEnv(t *testing.T) *testEnv {
|
||||
t.Helper()
|
||||
return newTestEnvAs(t, &auth.User{Role: auth.RoleGuest})
|
||||
}
|
||||
|
||||
// newTestEnvAs 注入会话身份,等价于真实链路里 Authenticate 中间件写进 context 的 User。
|
||||
func newTestEnvAs(t *testing.T, u *auth.User) *testEnv {
|
||||
t.Helper()
|
||||
|
||||
db := newTestDB(t)
|
||||
h := NewHub(db, slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError})))
|
||||
srv := httptest.NewServer(h.Handler())
|
||||
handler := h.Handler()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeHTTP(w, r.WithContext(middleware.WithUser(r.Context(), u)))
|
||||
}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
srv.Close()
|
||||
@@ -165,6 +178,24 @@ func (c *wsClient) recvEvent(want string) json.RawMessage {
|
||||
return nil
|
||||
}
|
||||
|
||||
// expectClosed 断言连接在 d 内被服务端关闭,且关闭前没有下发任何事件。
|
||||
func (c *wsClient) expectClosed(d time.Duration) {
|
||||
c.t.Helper()
|
||||
timer := time.NewTimer(d)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-c.msgs:
|
||||
if !ok || msg.err != nil {
|
||||
return
|
||||
}
|
||||
c.t.Fatalf("连接关闭前仍收到事件 %s", msg.event)
|
||||
case <-timer.C:
|
||||
c.t.Fatalf("等待服务端关闭连接超时")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decode[T any](t *testing.T, raw json.RawMessage) T {
|
||||
t.Helper()
|
||||
var v T
|
||||
@@ -458,12 +489,13 @@ func TestDisplayInstancesRegistry(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// 房间隔离:admin:<userId> 只发给对应管理端。
|
||||
// 房间隔离:admin:<userId> 只发给对应管理端,且 userId 来自会话而非客户端自报。
|
||||
func TestAdminRoomTargeting(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
env := newTestEnvAs(t, &auth.User{ID: 7, Username: "boss", Role: auth.RoleAdmin, Active: true})
|
||||
display := env.dial(t, &helloPayload{Role: RoleDisplay, InstanceID: "d1"})
|
||||
display.recvEvent(EventSnapshot)
|
||||
admin := env.dial(t, &helloPayload{Role: RoleAdmin, UserID: 7})
|
||||
// hello 不再携带 userId:定向房间必须用会话里的 ID。
|
||||
admin := env.dial(t, &helloPayload{Role: RoleAdmin})
|
||||
admin.recvEvent(EventSnapshot)
|
||||
|
||||
insertChange(t, env.db, "result", 1, ActionUpsert, `{"rank":1}`)
|
||||
@@ -478,6 +510,50 @@ func TestAdminRoomTargeting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// guest 自报 role=admin 必须被拒绝:管理端房间只能由会话授予。
|
||||
func TestGuestCannotSubscribeAdminRoom(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
c := env.dial(t, &helloPayload{Role: RoleAdmin})
|
||||
|
||||
c.expectClosed(3 * time.Second)
|
||||
}
|
||||
|
||||
// guest 只能订阅 display 房间:即使 hello 里塞了 userId 也不能进入定向房间。
|
||||
func TestGuestStaysInDisplayRoom(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
c := env.dial(t, nil)
|
||||
c.send(EventHello, map[string]any{"role": RoleDisplay, "userId": 99})
|
||||
c.recvEvent(EventSnapshot)
|
||||
|
||||
insertChange(t, env.db, "result", 1, ActionUpsert, `{"rank":1}`)
|
||||
changes := loadChanges(t, env.hub, 0, 0)
|
||||
env.hub.BroadcastTo(RoomAdmin, changes)
|
||||
env.hub.BroadcastTo(AdminRoom(99), changes)
|
||||
if ev, _, ok := c.recvWithin(300 * time.Millisecond); ok {
|
||||
t.Fatalf("guest 不应收到管理端房间的消息,收到 %s", ev)
|
||||
}
|
||||
|
||||
// display 房间的消息仍然正常到达。
|
||||
env.hub.BroadcastTo(RoomDisplay, changes)
|
||||
if got := decode[changePayload](t, c.recvEvent(EventChange)); got.Version != 1 {
|
||||
t.Fatalf("大屏应收到 display 房间消息,版本得到 %d", got.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// recorder 会话可以进管理端房间(大屏控制/数据同步都由管理端承担)。
|
||||
func TestRecorderSessionJoinsAdminRoom(t *testing.T) {
|
||||
env := newTestEnvAs(t, &auth.User{ID: 3, Username: "rec", Role: auth.RoleRecorder, Active: true})
|
||||
c := env.dial(t, &helloPayload{Role: RoleAdmin})
|
||||
c.recvEvent(EventSnapshot)
|
||||
|
||||
insertChange(t, env.db, "result", 1, ActionUpsert, `{"rank":1}`)
|
||||
env.hub.BroadcastTo(RoomAdmin, loadChanges(t, env.hub, 0, 0))
|
||||
|
||||
if got := decode[changePayload](t, c.recvEvent(EventChange)); got.Version != 1 {
|
||||
t.Fatalf("recorder 应进入管理端房间,版本得到 %d", got.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// 慢客户端:ack 落后超过阈值即被识别并断开。
|
||||
func TestSlowClientDetected(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
|
||||
Reference in new issue
Block a user