chore: directory migration - gateway→server, web→client/electron

This commit is contained in:
2026-09-08 19:16:35 +08:00
parent fd9f99a3f9
commit f9d757b5e5
243 changed files with 5095 additions and 228 deletions

View File

@ -0,0 +1,108 @@
package sse
import (
"net/http/httptest"
"strings"
"testing"
)
// 一个可 Flush 的 ResponseWriter供集成测试用。
type flushWriter struct {
*httptest.ResponseRecorder
flushed chan bool
}
func (f *flushWriter) Flush() {
select {
case f.flushed <- true:
default:
}
}
// 端到端验证Manager 完整走一遍「事件入缓冲区 → 新连接带 Last-Event-ID 重连 → 补投」。
// 这是生产里最关键的可靠性路径 —— 断线期间收的邮件,重连后必须能看到。
func TestManagerReplayOnReconnect(t *testing.T) {
m := &Manager{
clients: make(map[string]*Client),
eventBuffer: make(map[string]*eventRing),
}
// 1) 用户 alice 发来一封信(无人连接也入缓冲区)
m.SendToUser("alice", "new_mail", map[string]string{"mail_id": "m1"})
m.SendToUser("alice", "new_mail", map[string]string{"mail_id": "m2"})
m.SendToUser("alice", "new_mail", map[string]string{"mail_id": "m3"})
ring := m.eventBuffer["u:alice"]
if ring == nil {
t.Fatal("alice 的缓冲区应该已创建")
}
// 2) 带 Last-Event-ID=1 重连,应补投 m2、m3跳过 m1
req := httptest.NewRequest("GET", "/events/stream", nil)
req.Header.Set("Last-Event-ID", "1")
w := &flushWriter{httptest.NewRecorder(), make(chan bool, 10)}
client := m.AddClient(w, req, "", "alice")
if client == nil {
t.Fatal("AddClient 返回 nilflushWriter 应支持 Flush")
}
defer m.RemoveClient(client.ID)
body := w.Body.String()
if !strings.Contains(body, `"mail_id":"m2"`) {
t.Error("重连后应补投 m2实际 body:", body)
}
if !strings.Contains(body, `"mail_id":"m3"`) {
t.Error("重连后应补投 m3实际 body:", body)
}
if strings.Contains(body, `"mail_id":"m1"`) {
t.Error("已确认的 m1 不应重放Last-Event-ID=1实际 body:", body)
}
// 3) 连接期间新来一封信,实时推送
m.SendToUser("alice", "new_mail", map[string]string{"mail_id": "m4"})
body = w.Body.String()
if !strings.Contains(body, `"mail_id":"m4"`) {
t.Error("在线连接应实时收到 m4实际 body:", body)
}
}
// 序列号全局递增,两条不同事件不同 ID。
func TestEventIDMonotonic(t *testing.T) {
m := &Manager{
clients: make(map[string]*Client),
eventBuffer: make(map[string]*eventRing),
}
a := m.nextEventID()
b := m.nextEventID()
if a == b {
t.Fatalf("两个连续事件 ID 相同: %q", a)
}
if a > b {
t.Fatalf("事件 ID 应递增: %q > %q", a, b)
}
}
// 确保事件 ID 写进了 SSE 帧EventSource 靠 id: 行记住位置)
func TestSendWritesIDField(t *testing.T) {
fw := &flushWriter{httptest.NewRecorder(), make(chan bool, 5)}
c := &Client{
ID: "c1",
UserName: "alice",
Res: fw,
Flusher: fw,
done: make(chan struct{}),
}
c.SendWithID("42", "new_mail", map[string]string{"x": "y"})
body := fw.Body.String()
if !strings.Contains(body, "id: 42\n") {
t.Error("帧里应有 id: 42 行,实际:", body)
}
if !strings.Contains(body, "event: new_mail") {
t.Error("帧里应有 event: new_mail实际:", body)
}
if !strings.Contains(body, `data: {"x":"y"}`) {
t.Error("帧里应有 data实际:", body)
}
}