feat(messages): сообщения, реакции, пины, typing, read states и поиск
Бэкенд текстовой связи (AGENT.md 7.6, 7.15, 7.16, 8.6):
- POST/GET/PATCH/DELETE /channels/{id}/messages, ответы и упоминания,
окно правки 24 часа, права автора или MANAGE_MESSAGES;
- реакции (PUT/DELETE .../reactions/{emoji}) с агрегацией и отметкой «моя»;
- закрепления (GET/PUT/DELETE .../pins) под MANAGE_MESSAGES;
- typing с лимитом 1/3 с и адресной рассылкой всем, кроме автора;
- read states: POST /channels/{id}/ack, синхронизация READ_STATE_UPDATE между
устройствами пользователя;
- поиск FTS5 по комнате (10/мин) с экранированием запроса;
- лимиты отправки 5/5 с (burst 10) и slowmode комнаты; администратор инстанса
обходит и то, и другое (AGENT.md 7.19);
- Gateway: DispatchToChannel/DispatchToChannelExcept доставляют события комнат
только тем, кто видит комнату (VIEW_CHANNEL), права считает общий движок;
- store: messages, message_reactions, channel_read_states, files (загрузка
файлов появится вместе с вложениями).
Тесты: жизненный цикл сообщения, видимость скрытой комнаты (404 участнику,
200 админу), slowmode, упоминания и ответы, typing и read state, фильтрация
событий комнаты между двумя WS-клиентами.
This commit is contained in:
@@ -175,6 +175,54 @@ func TestRealtimeEventsReachConnectedClients(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// expectNoEvent проверяет, что за отведённое время событие не пришло.
|
||||
func (c *realtimeClient) expectNoEvent(event string, wait time.Duration) {
|
||||
c.t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), wait)
|
||||
defer cancel()
|
||||
for {
|
||||
_, data, err := c.conn.Read(ctx)
|
||||
if err != nil {
|
||||
// Таймаут — то, что нужно: событие не доставлено.
|
||||
return
|
||||
}
|
||||
var frame realtimeFrame
|
||||
if err := json.Unmarshal(data, &frame); err != nil {
|
||||
c.t.Fatalf("decode frame: %v", err)
|
||||
}
|
||||
if frame.Op == 0 && frame.T == event {
|
||||
c.t.Fatalf("event %s must not reach this client", event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelEventsFilteredByPermissions проверяет фильтрацию событий комнат:
|
||||
// сообщения скрытой комнаты не уходят тем, кто её не видит (AGENT.md 8.3, 9.7).
|
||||
func TestChannelEventsFilteredByPermissions(t *testing.T) {
|
||||
f := newMessagingFixture(t)
|
||||
httpServer := httptest.NewServer(f.srv.Handler())
|
||||
t.Cleanup(httpServer.Close)
|
||||
|
||||
owner := dialGateway(t, httpServer, f.ownerCookie)
|
||||
member := dialGateway(t, httpServer, f.memberCookie)
|
||||
|
||||
// Открытая комната: событие получают оба.
|
||||
if rec := doJSON(t, f.srv, http.MethodPost, "/api/v1/channels/"+f.openChannel+"/messages",
|
||||
`{"content":"всем"}`, f.ownerCookie); rec.Code != http.StatusOK {
|
||||
t.Fatalf("open channel message = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
owner.expectEvent("MESSAGE_CREATE")
|
||||
member.expectEvent("MESSAGE_CREATE")
|
||||
|
||||
// Скрытая комната: владелец получает событие, участник — нет.
|
||||
if rec := doJSON(t, f.srv, http.MethodPost, "/api/v1/channels/"+f.secretID+"/messages",
|
||||
`{"content":"только для владельца"}`, f.ownerCookie); rec.Code != http.StatusOK {
|
||||
t.Fatalf("secret channel message = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
owner.expectEvent("MESSAGE_CREATE")
|
||||
member.expectNoEvent("MESSAGE_CREATE", 700*time.Millisecond)
|
||||
}
|
||||
|
||||
// TestInstanceAdminManagesForeignGuild проверяет §11.5: администратор инстанса,
|
||||
// не состоящий в сервере, видит и меняет всё, а его самого модерировать нельзя.
|
||||
func TestInstanceAdminManagesForeignGuild(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user