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 返回 nil(flushWriter 应支持 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) } }