Этап 2: чат 1:1 — устройства, очередь, SSE, шифрование сообщений
Сервер: регистрация устройств и X-Device, hub с одним потоком на устройство, очередь per-device с фан-аутом без эха отправителю, POST /api/messages с проверками в порядке protocol.md, ACK, SSE с воспроизведением очереди, ready и пингом раз в 20 секунд, контакты в обе стороны при первом сообщении, лимит 30 сообщений в минуту. Клиент: ULID, ключ 1:1 из ECDH через HKDF, шифрование конверта с AAD, sync.js как единственный писатель в IndexedDB, ACK строго после записи, список чатов, экран чата по эталону, разделители дат и «новые», pending и failed с повтором, полоса «нет соединения». ADR-033: у неотправленного есть текст отказа — clock_skew стало видно. ADR-034: входящее с известным id не перезаписывает запись. Собеседник знает открытый id конверта и подменял им чужое сообщение в чужой истории — вплоть до стирания своего присланного, чего «удалить у всех не существует» не допускает. ADR-035: один поток событий на браузерный профиль (locks + BroadcastChannel): две вкладки отбирали поток друг у друга и оставались без живой доставки. ADR-036: повтор отправки сохраняет ULID, пока он в пределах окна часов, — иначе потерянный ответ давал у собеседника два сообщения вместо одного. Приёмка на боевом сервере: два аккаунта, пять устройств, живая доставка, копия на второе устройство, очередь офлайн-устройству, ACK, подмена from игнорируется, чужой deviceId и запрос без Origin отбиваются, плейнтекста в базе и WAL ноль вхождений. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/xmatic-squad/bare/internal/api"
|
||||
@@ -23,7 +24,34 @@ type env struct {
|
||||
t *testing.T
|
||||
h http.Handler
|
||||
st *store.Store
|
||||
log *bytes.Buffer
|
||||
log *syncLog
|
||||
srv *httptest.Server
|
||||
}
|
||||
|
||||
// syncLog — журнал сервера в памяти. Под замком, потому что пишут в него
|
||||
// и обработчики, вызванные напрямую, и обработчики настоящего сервера
|
||||
// из live: у них разные горутины.
|
||||
type syncLog struct {
|
||||
mu sync.Mutex
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (l *syncLog) Write(p []byte) (int, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.buf.Write(p)
|
||||
}
|
||||
|
||||
func (l *syncLog) String() string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.buf.String()
|
||||
}
|
||||
|
||||
func (l *syncLog) Reset() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.buf.Reset()
|
||||
}
|
||||
|
||||
func newEnv(t *testing.T) *env { return invited(t, "") }
|
||||
@@ -48,7 +76,7 @@ func invited(t *testing.T, code string) *env {
|
||||
VAPIDPublic: "vapid",
|
||||
InviteCode: code,
|
||||
}
|
||||
e := &env{t: t, st: st, log: &bytes.Buffer{}}
|
||||
e := &env{t: t, st: st, log: &syncLog{}}
|
||||
e.h = api.New(cfg, st, static, e.log)
|
||||
return e
|
||||
}
|
||||
@@ -81,6 +109,18 @@ func (e *env) do(method, target string, body any, opts ...func(*http.Request)) *
|
||||
return rec
|
||||
}
|
||||
|
||||
// live поднимает настоящий сервер на том же обработчике. Нужен потоку
|
||||
// событий: httptest.ResponseRecorder не отдаёт тело, пока обработчик
|
||||
// не вернулся, а поток не возвращается никогда.
|
||||
func (e *env) live() *httptest.Server {
|
||||
e.t.Helper()
|
||||
if e.srv == nil {
|
||||
e.srv = httptest.NewServer(e.h)
|
||||
e.t.Cleanup(e.srv.Close)
|
||||
}
|
||||
return e.srv
|
||||
}
|
||||
|
||||
func with(c *http.Cookie) func(*http.Request) {
|
||||
return func(r *http.Request) {
|
||||
if c != nil {
|
||||
@@ -89,6 +129,10 @@ func with(c *http.Cookie) func(*http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func withDevice(id string) func(*http.Request) {
|
||||
return func(r *http.Request) { r.Header.Set("X-Device", id) }
|
||||
}
|
||||
|
||||
func withOrigin(value string) func(*http.Request) {
|
||||
return func(r *http.Request) {
|
||||
if value == "" {
|
||||
|
||||
Reference in New Issue
Block a user