Skip to content

Commit d842670

Browse files
authored
Regression test for /sync returning 404 for redactions (#386)
Lots more detail in the code, but this is a regression test for a bug in Synapse where it would return a 404 from /sync if given a since token which pointed to a redaction of an unknown event.
1 parent 2503a6d commit d842670

File tree

4 files changed

+121
-1
lines changed

4 files changed

+121
-1
lines changed

internal/b/blueprints.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ type Event struct {
110110
// The prev events of the event if we want to override or falsify them.
111111
// If it is left at nil, MustCreateEvent will populate it automatically based on the forward extremities.
112112
PrevEvents interface{}
113+
114+
// If this is a redaction, the event that it redacts
115+
Redacts string
113116
}
114117

115118
func MustValidate(bp Blueprint) Blueprint {

internal/client/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ func (c *CSAPI) MustSync(t *testing.T, syncReq SyncReq) (gjson.Result, string) {
248248
// In the unlikely event that you need ordering on your checks, call MustSyncUntil multiple times
249249
// with a single checker, and reuse the returned since token, as in the "Incremental sync" example.
250250
//
251-
// Will time out after CSAPI.SyncUntilTimeout. Returns the latest since token used.
251+
// Will time out after CSAPI.SyncUntilTimeout. Returns the `next_batch` token from the final
252+
// response.
252253
func (c *CSAPI) MustSyncUntil(t *testing.T, syncReq SyncReq, checks ...SyncCheckOpt) string {
253254
t.Helper()
254255
start := time.Now()

internal/federation/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ func (s *Server) MustCreateEvent(t *testing.T, room *ServerRoom, ev b.Event) *go
263263
PrevEvents: prevEvents,
264264
Unsigned: unsigned,
265265
AuthEvents: ev.AuthEvents,
266+
Redacts: ev.Redacts,
266267
}
267268
if eb.AuthEvents == nil {
268269
var stateNeeded gomatrixserverlib.StateNeeded

tests/csapi/sync_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/matrix-org/complement/internal/b"
1212
"github.com/matrix-org/complement/internal/client"
13+
"github.com/matrix-org/complement/internal/federation"
1314
"github.com/matrix-org/complement/runtime"
1415
)
1516

@@ -251,6 +252,120 @@ func TestSync(t *testing.T) {
251252
res, _ := alice.MustSync(t, client.SyncReq{Since: nextBatch})
252253
usersInPresenceEvents(t, res.Get("presence"), []string{})
253254
})
255+
256+
t.Run("sync should succeed even if the sync token points to a redaction of an unknown event", func(t *testing.T) {
257+
// this is a regression test for https://github.com/matrix-org/synapse/issues/12864
258+
//
259+
// The idea here is that we need a sync token which points to a redaction
260+
// for an event which doesn't exist. Such a redaction may not be served to
261+
// the client. This can lead to server bugs when the server tries to fetch
262+
// the event corresponding to the sync token.
263+
//
264+
// The C-S API does not permit us to generate such a redaction event, so
265+
// we have to poke it in from a federated server.
266+
//
267+
// The situation is complicated further by the very fact that we
268+
// cannot see the faulty redaction, and therefore cannot tell whether
269+
// our sync token includes it or not. The normal trick here would be
270+
// to send another (regular) event as a sentinel, and then if that sentinel
271+
// is returned by /sync, we can be sure the faulty event has also been
272+
// processed. However, that doesn't work here, because doing so will mean
273+
// that the sync token points to the sentinel rather than the redaction,
274+
// negating the whole point of the test.
275+
//
276+
// Instead, as a rough proxy, we send a sentinel in a *different* room.
277+
// There is no guarantee that the target server will process the events
278+
// in the order we send them, but in practice it seems to get close
279+
// enough.
280+
281+
t.Parallel()
282+
283+
// alice creates two rooms, which charlie (on our test server) joins
284+
srv := federation.NewServer(t, deployment,
285+
federation.HandleKeyRequests(),
286+
federation.HandleTransactionRequests(nil, nil),
287+
)
288+
cancel := srv.Listen()
289+
defer cancel()
290+
291+
charlie := srv.UserID("charlie")
292+
293+
redactionRoomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
294+
redactionRoom := srv.MustJoinRoom(t, deployment, "hs1", redactionRoomID, charlie)
295+
296+
sentinelRoomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
297+
sentinelRoom := srv.MustJoinRoom(t, deployment, "hs1", sentinelRoomID, charlie)
298+
299+
// charlie creates a bogus redaction, which he sends out, followed by
300+
// a good event - in another room - to act as a sentinel. It's not
301+
// guaranteed, but hopefully if the sentinel is received, so was the
302+
// redaction.
303+
redactionEvent := srv.MustCreateEvent(t, redactionRoom, b.Event{
304+
Type: "m.room.redaction",
305+
Sender: charlie,
306+
Content: map[string]interface{}{},
307+
Redacts: "$12345",
308+
})
309+
redactionRoom.AddEvent(redactionEvent)
310+
t.Logf("Created redaction event %s", redactionEvent.EventID())
311+
srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{redactionEvent.JSON()}, nil)
312+
313+
sentinelEvent := srv.MustCreateEvent(t, sentinelRoom, b.Event{
314+
Type: "m.room.test",
315+
Sender: charlie,
316+
Content: map[string]interface{}{"body": "1234"},
317+
})
318+
sentinelRoom.AddEvent(sentinelEvent)
319+
srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{redactionEvent.JSON(), sentinelEvent.JSON()}, nil)
320+
321+
// wait for the sentinel to arrive
322+
nextBatch := alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(sentinelRoomID, sentinelEvent.EventID()))
323+
324+
// charlie sends another batch of events to force a gappy sync.
325+
// We have to send 11 events to force a gap, since we use a filter with a timeline limit of 10 events.
326+
pdus := make([]json.RawMessage, 11)
327+
var lastSentEventId string
328+
for i := range pdus {
329+
ev := srv.MustCreateEvent(t, redactionRoom, b.Event{
330+
Type: "m.room.message",
331+
Sender: charlie,
332+
Content: map[string]interface{}{},
333+
})
334+
redactionRoom.AddEvent(ev)
335+
pdus[i] = ev.JSON()
336+
lastSentEventId = ev.EventID()
337+
}
338+
srv.MustSendTransaction(t, deployment, "hs1", pdus, nil)
339+
t.Logf("Sent filler events, with final event %s", lastSentEventId)
340+
341+
// sync, starting from the same ?since each time, until the final message turns up.
342+
// This is basically an inlining of MustSyncUntil, with the key difference that we
343+
// keep the same ?since each time, instead of incrementally syncing on each pass.
344+
numResponsesReturned := 0
345+
start := time.Now()
346+
for {
347+
if time.Since(start) > alice.SyncUntilTimeout {
348+
t.Fatalf("%s: timed out after %v. Seen %d /sync responses", alice.UserID, time.Since(start), numResponsesReturned)
349+
}
350+
// sync, using a filter with a limit smaller than the number of PDUs we sent.
351+
syncResponse, _ := alice.MustSync(t, client.SyncReq{Filter: filterID, Since: nextBatch})
352+
numResponsesReturned += 1
353+
timeline := syncResponse.Get("rooms.join." + client.GjsonEscape(redactionRoomID) + ".timeline")
354+
timelineEvents := timeline.Get("events").Array()
355+
lastEventIdInSync := timelineEvents[len(timelineEvents)-1].Get("event_id").String()
356+
357+
t.Logf("Iteration %d: /sync returned %d events, with final event %s", numResponsesReturned, len(timelineEvents), lastEventIdInSync)
358+
if lastEventIdInSync == lastSentEventId {
359+
// check we actually got a gappy sync - else this test isn't testing the right thing
360+
if !timeline.Get("limited").Bool() {
361+
t.Fatalf("Not a gappy sync after redaction")
362+
}
363+
break
364+
}
365+
}
366+
367+
// that's it - we successfully did a gappy sync.
368+
})
254369
})
255370
}
256371

0 commit comments

Comments
 (0)