sliding-sync/state/storage_test.go

1139 lines
42 KiB
Go
Raw Normal View History

package state
import (
"bytes"
"context"
"encoding/json"
2023-07-17 16:25:28 +01:00
"fmt"
"math/rand"
"reflect"
"sort"
"testing"
2021-10-26 10:01:45 +01:00
"time"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/sliding-sync/sync2"
"github.com/jmoiron/sqlx"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/sliding-sync/internal"
"github.com/matrix-org/sliding-sync/sqlutil"
"github.com/matrix-org/sliding-sync/testutils"
"github.com/tidwall/gjson"
)
func TestStorageRoomStateBeforeAndAfterEventPosition(t *testing.T) {
ctx := context.Background()
store := NewStorage(postgresConnectionString)
defer store.Teardown()
roomID := "!TestStorageRoomStateAfterEventPosition:localhost"
alice := "@alice:localhost"
bob := "@bob:localhost"
events := []json.RawMessage{
testutils.NewStateEvent(t, "m.room.create", "", alice, map[string]interface{}{"creator": alice}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, alice),
testutils.NewStateEvent(t, "m.room.join_rules", "", alice, map[string]interface{}{"join_rule": "invite"}),
testutils.NewStateEvent(t, "m.room.member", bob, alice, map[string]interface{}{"membership": "invite"}),
}
2023-09-19 12:41:25 +01:00
accResult, err := store.Accumulate(userID, roomID, sync2.TimelineResponse{Events: events})
if err != nil {
t.Fatalf("Accumulate returned error: %s", err)
}
2023-09-07 19:21:44 +01:00
latest := accResult.TimelineNIDs[len(accResult.TimelineNIDs)-1]
testCases := []struct {
name string
getEvents func() []Event
wantEvents []json.RawMessage
}{
{
name: "room state after the latest position includes the invite event",
getEvents: func() []Event {
events, err := store.RoomStateAfterEventPosition(ctx, []string{roomID}, latest, nil)
if err != nil {
t.Fatalf("RoomStateAfterEventPosition: %s", err)
}
return events[roomID]
},
wantEvents: events[:],
},
{
name: "room state after the latest position filtered for join_rule returns a single event",
getEvents: func() []Event {
events, err := store.RoomStateAfterEventPosition(ctx, []string{roomID}, latest, map[string][]string{"m.room.join_rules": nil})
if err != nil {
t.Fatalf("RoomStateAfterEventPosition: %s", err)
}
return events[roomID]
},
wantEvents: []json.RawMessage{
events[2],
},
},
{
name: "room state after the latest position filtered for join_rule and create event excludes member events",
getEvents: func() []Event {
events, err := store.RoomStateAfterEventPosition(ctx, []string{roomID}, latest, map[string][]string{
"m.room.join_rules": []string{""},
"m.room.create": nil, // all matching state events with this event type
})
if err != nil {
t.Fatalf("RoomStateAfterEventPosition: %s", err)
}
return events[roomID]
},
wantEvents: []json.RawMessage{
events[0], events[2],
},
},
{
name: "room state after the latest position filtered for all members returns all member events",
getEvents: func() []Event {
events, err := store.RoomStateAfterEventPosition(ctx, []string{roomID}, latest, map[string][]string{
"m.room.member": nil, // all matching state events with this event type
})
if err != nil {
t.Fatalf("RoomStateAfterEventPosition: %s", err)
}
return events[roomID]
},
wantEvents: []json.RawMessage{
events[1], events[3],
},
},
}
for _, tc := range testCases {
gotEvents := tc.getEvents()
if len(gotEvents) != len(tc.wantEvents) {
t.Errorf("%s: got %d events want %d : got %+v", tc.name, len(gotEvents), len(tc.wantEvents), gotEvents)
continue
}
for i, eventJSON := range tc.wantEvents {
if !bytes.Equal(eventJSON, gotEvents[i].JSON) {
t.Errorf("%s: pos %d\ngot %s\nwant %s", tc.name, i, string(gotEvents[i].JSON), string(eventJSON))
}
}
}
}
func TestStorageJoinedRoomsAfterPosition(t *testing.T) {
2023-06-05 18:10:35 +01:00
// Clean DB. If we don't, other tests' events will be in the DB, but we won't
// provide keys in the metadata dict we pass to MetadataForAllRooms, leading to a
// panic.
if err := cleanDB(t); err != nil {
t.Fatalf("failed to wipe DB: %s", err)
}
store := NewStorage(postgresConnectionString)
defer store.Teardown()
joinedRoomID := "!joined:bar"
invitedRoomID := "!invited:bar"
leftRoomID := "!left:bar"
banRoomID := "!ban:bar"
bobJoinedRoomID := "!bobjoined:bar"
alice := "@aliceTestStorageJoinedRoomsAfterPosition:localhost"
bob := "@bobTestStorageJoinedRoomsAfterPosition:localhost"
charlie := "@charlieTestStorageJoinedRoomsAfterPosition:localhost"
roomIDToEventMap := map[string][]json.RawMessage{
joinedRoomID: {
testutils.NewStateEvent(t, "m.room.create", "", alice, map[string]interface{}{"creator": alice}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, alice),
},
invitedRoomID: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, bob),
testutils.NewStateEvent(t, "m.room.member", alice, bob, map[string]interface{}{"membership": "invite"}),
},
leftRoomID: {
testutils.NewStateEvent(t, "m.room.create", "", alice, map[string]interface{}{"creator": alice}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, alice),
testutils.NewStateEvent(t, "m.room.member", alice, alice, map[string]interface{}{"membership": "leave"}),
},
banRoomID: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, bob),
testutils.NewJoinEvent(t, alice),
testutils.NewStateEvent(t, "m.room.member", alice, bob, map[string]interface{}{"membership": "ban"}),
},
bobJoinedRoomID: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, bob),
testutils.NewJoinEvent(t, charlie),
},
}
var latestPos int64
var err error
for roomID, eventMap := range roomIDToEventMap {
2023-09-19 12:41:25 +01:00
accResult, err := store.Accumulate(userID, roomID, sync2.TimelineResponse{Events: eventMap})
if err != nil {
t.Fatalf("Accumulate on %s failed: %s", roomID, err)
}
2023-09-07 19:21:44 +01:00
latestPos = accResult.TimelineNIDs[len(accResult.TimelineNIDs)-1]
}
aliceJoinTimingsByRoomID, err := store.JoinedRoomsAfterPosition(alice, latestPos)
if err != nil {
t.Fatalf("failed to JoinedRoomsAfterPosition: %s", err)
}
if len(aliceJoinTimingsByRoomID) != 1 {
t.Fatalf("JoinedRoomsAfterPosition at %v for %s got %v, want room %s only", latestPos, alice, aliceJoinTimingsByRoomID, joinedRoomID)
}
for gotRoomID, _ := range aliceJoinTimingsByRoomID {
2023-06-05 14:03:30 +01:00
if gotRoomID != joinedRoomID {
t.Fatalf("JoinedRoomsAfterPosition at %v for %s got %v want %v", latestPos, alice, gotRoomID, joinedRoomID)
}
}
bobJoinTimingsByRoomID, err := store.JoinedRoomsAfterPosition(bob, latestPos)
if err != nil {
t.Fatalf("failed to JoinedRoomsAfterPosition: %s", err)
}
if len(bobJoinTimingsByRoomID) != 3 {
t.Fatalf("JoinedRoomsAfterPosition for %s got %v rooms want %v", bob, len(bobJoinTimingsByRoomID), 3)
}
// also test currentNotMembershipStateEventsInAllRooms
add extensions for typing and receipts; bugfixes and additional perf improvements Features: - Add `typing` extension. - Add `receipts` extension. - Add comprehensive prometheus `/metrics` activated via `SYNCV3_PROM`. - Add `SYNCV3_PPROF` support. - Add `by_notification_level` sort order. - Add `include_old_rooms` support. - Add support for `$ME` and `$LAZY`. - Add correct filtering when `*,*` is used as `required_state`. - Add `num_live` to each room response to indicate how many timeline entries are live. Bug fixes: - Use a stricter comparison function on ranges: fixes an issue whereby UTs fail on go1.19 due to change in sorting algorithm. - Send back an `errcode` on HTTP errors (e.g expired sessions). - Remove `unsigned.txn_id` on insertion into the DB. Otherwise other users would see other users txn IDs :( - Improve range delta algorithm: previously it didn't handle cases like `[0,20] -> [20,30]` and would panic. - Send HTTP 400 for invalid range requests. - Don't publish no-op unread counts which just adds extra noise. - Fix leaking DB connections which could eventually consume all available connections. - Ensure we always unblock WaitUntilInitialSync even on invalid access tokens. Other code relies on WaitUntilInitialSync() actually returning at _some_ point e.g on startup we have N workers which bound the number of concurrent pollers made at any one time, we need to not just hog a worker forever. Improvements: - Greatly improve startup times of sync3 handlers by improving `JoinedRoomsTracker`: a modest amount of data would take ~28s to create the handler, now it takes 4s. - Massively improve initial initial v3 sync times, by refactoring `JoinedRoomsTracker`, from ~47s to <1s. - Add `SlidingSyncUntil...` in tests to reduce races. - Tweak the API shape of JoinedUsersForRoom to reduce state block processing time for large rooms from 63s to 39s. - Add trace task for initial syncs. - Include the proxy version in UA strings. - HTTP errors now wait 1s before returning to stop clients tight-looping on error. - Pending event buffer is now 2000. - Index the room ID first to cull the most events when returning timeline entries. Speeds up `SelectLatestEventsBetween` by a factor of 8. - Remove cancelled `m.room_key_requests` from the to-device inbox. Cuts down the amount of events in the inbox by ~94% for very large (20k+) inboxes, ~50% for moderate sized (200 events) inboxes. Adds book-keeping to remember the unacked to-device position for each client.
2022-12-14 18:53:55 +00:00
txn := store.DB.MustBeginTx(context.Background(), nil)
roomIDToCreateEvents, err := store.currentNotMembershipStateEventsInAllRooms(txn, []string{"m.room.create"})
if err != nil {
t.Fatalf("CurrentStateEventsInAllRooms returned error: %s", err)
}
for roomID := range roomIDToEventMap {
if _, ok := roomIDToCreateEvents[roomID]; !ok {
t.Fatalf("CurrentStateEventsInAllRooms missed room ID %s", roomID)
}
}
for roomID := range roomIDToEventMap {
createEvents := roomIDToCreateEvents[roomID]
if createEvents == nil {
t.Errorf("CurrentStateEventsInAllRooms: unknown room %v", roomID)
}
if len(createEvents) != 1 {
t.Fatalf("CurrentStateEventsInAllRooms got %d events, want 1", len(createEvents))
}
if len(createEvents[0].JSON) < 20 { // make sure there's something here
t.Errorf("CurrentStateEventsInAllRooms: got wrong json for event, got %s", string(createEvents[0].JSON))
}
}
newMetadata := func(roomID string, joinCount, inviteCount int) internal.RoomMetadata {
2023-06-05 14:03:30 +01:00
m := internal.NewRoomMetadata(roomID)
m.JoinCount = joinCount
m.InviteCount = inviteCount
2023-06-05 14:03:30 +01:00
return *m
}
// also test MetadataForAllRooms
roomIDToMetadata := map[string]internal.RoomMetadata{
joinedRoomID: newMetadata(joinedRoomID, 1, 0),
invitedRoomID: newMetadata(invitedRoomID, 1, 1),
banRoomID: newMetadata(banRoomID, 1, 0),
bobJoinedRoomID: newMetadata(bobJoinedRoomID, 2, 0),
}
2023-06-05 14:03:30 +01:00
tempTableName, err := store.PrepareSnapshot(txn)
if err != nil {
t.Fatalf("PrepareSnapshot: %s", err)
}
err = store.MetadataForAllRooms(txn, tempTableName, roomIDToMetadata)
add extensions for typing and receipts; bugfixes and additional perf improvements Features: - Add `typing` extension. - Add `receipts` extension. - Add comprehensive prometheus `/metrics` activated via `SYNCV3_PROM`. - Add `SYNCV3_PPROF` support. - Add `by_notification_level` sort order. - Add `include_old_rooms` support. - Add support for `$ME` and `$LAZY`. - Add correct filtering when `*,*` is used as `required_state`. - Add `num_live` to each room response to indicate how many timeline entries are live. Bug fixes: - Use a stricter comparison function on ranges: fixes an issue whereby UTs fail on go1.19 due to change in sorting algorithm. - Send back an `errcode` on HTTP errors (e.g expired sessions). - Remove `unsigned.txn_id` on insertion into the DB. Otherwise other users would see other users txn IDs :( - Improve range delta algorithm: previously it didn't handle cases like `[0,20] -> [20,30]` and would panic. - Send HTTP 400 for invalid range requests. - Don't publish no-op unread counts which just adds extra noise. - Fix leaking DB connections which could eventually consume all available connections. - Ensure we always unblock WaitUntilInitialSync even on invalid access tokens. Other code relies on WaitUntilInitialSync() actually returning at _some_ point e.g on startup we have N workers which bound the number of concurrent pollers made at any one time, we need to not just hog a worker forever. Improvements: - Greatly improve startup times of sync3 handlers by improving `JoinedRoomsTracker`: a modest amount of data would take ~28s to create the handler, now it takes 4s. - Massively improve initial initial v3 sync times, by refactoring `JoinedRoomsTracker`, from ~47s to <1s. - Add `SlidingSyncUntil...` in tests to reduce races. - Tweak the API shape of JoinedUsersForRoom to reduce state block processing time for large rooms from 63s to 39s. - Add trace task for initial syncs. - Include the proxy version in UA strings. - HTTP errors now wait 1s before returning to stop clients tight-looping on error. - Pending event buffer is now 2000. - Index the room ID first to cull the most events when returning timeline entries. Speeds up `SelectLatestEventsBetween` by a factor of 8. - Remove cancelled `m.room_key_requests` from the to-device inbox. Cuts down the amount of events in the inbox by ~94% for very large (20k+) inboxes, ~50% for moderate sized (200 events) inboxes. Adds book-keeping to remember the unacked to-device position for each client.
2022-12-14 18:53:55 +00:00
txn.Commit()
if err != nil {
t.Fatalf("MetadataForAllRooms: %s", err)
}
wantHeroInfos := map[string]internal.RoomMetadata{
joinedRoomID: {
JoinCount: 1,
},
invitedRoomID: {
JoinCount: 1,
InviteCount: 1,
},
banRoomID: {
JoinCount: 1,
},
bobJoinedRoomID: {
JoinCount: 2,
},
}
for roomID, wantHI := range wantHeroInfos {
gotHI := roomIDToMetadata[roomID]
if gotHI.InviteCount != wantHI.InviteCount {
t.Errorf("hero info for %s got %d invited users, want %d", roomID, gotHI.InviteCount, wantHI.InviteCount)
}
if gotHI.JoinCount != wantHI.JoinCount {
t.Errorf("hero info for %s got %d joined users, want %d", roomID, gotHI.JoinCount, wantHI.JoinCount)
}
}
}
// Test the examples on VisibleEventNIDsBetween docs
func TestVisibleEventNIDsBetween(t *testing.T) {
store := NewStorage(postgresConnectionString)
defer store.Teardown()
roomA := "!a:localhost"
roomB := "!b:localhost"
roomC := "!c:localhost"
roomD := "!d:localhost"
roomE := "!e:localhost"
alice := "@alice_TestVisibleEventNIDsBetween:localhost"
bob := "@bob_TestVisibleEventNIDsBetween:localhost"
// bob makes all these rooms first, alice is already joined to C
roomIDToEventMap := map[string][]json.RawMessage{
roomA: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, bob),
},
roomB: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, bob),
},
roomC: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, bob),
testutils.NewJoinEvent(t, alice),
},
roomD: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, bob),
},
roomE: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, bob),
},
}
for roomID, eventMap := range roomIDToEventMap {
2023-04-17 18:21:58 +01:00
_, err := store.Initialise(roomID, eventMap)
if err != nil {
t.Fatalf("Initialise on %s failed: %s", roomID, err)
}
}
startPos, err := store.LatestEventNID()
if err != nil {
t.Fatalf("LatestEventNID: %s", err)
}
baseTimestamp := spec.Timestamp(1632131678061).Time()
// Test the examples
// Stream Positions
// 1 2 3 4 5 6 7 8 9 10
// Room A Maj E E E
// Room B E Maj E
// Room C E Mal E (a already joined to this room)
timelineInjections := []struct {
RoomID string
Events []json.RawMessage
}{
{
RoomID: roomA,
Events: []json.RawMessage{
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, alice),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
},
},
{
RoomID: roomB,
Events: []json.RawMessage{
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, alice),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
},
},
{
RoomID: roomA,
Events: []json.RawMessage{
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
},
},
{
RoomID: roomC,
Events: []json.RawMessage{
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
testutils.NewStateEvent(t, "m.room.member", alice, alice, map[string]interface{}{"membership": "leave"}),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
},
},
}
for _, tl := range timelineInjections {
2023-09-19 12:41:25 +01:00
accResult, err := store.Accumulate(userID, tl.RoomID, sync2.TimelineResponse{Events: tl.Events})
if err != nil {
t.Fatalf("Accumulate on %s failed: %s", tl.RoomID, err)
}
2023-09-07 19:21:44 +01:00
t.Logf("%s added %d new events", tl.RoomID, accResult.NumNew)
}
latestPos, err := store.LatestEventNID()
if err != nil {
t.Fatalf("LatestEventNID: %s", err)
}
t.Logf("ABC Start=%d Latest=%d", startPos, latestPos)
roomIDToVisibleRange, err := store.VisibleEventNIDsBetween(alice, startPos, latestPos)
if err != nil {
t.Fatalf("VisibleEventNIDsBetween to %d: %s", latestPos, err)
}
for roomID, r := range roomIDToVisibleRange {
t.Logf("%v => [%d,%d]", roomID, r[0]-startPos, r[1]-startPos)
}
if len(roomIDToVisibleRange) != 3 {
t.Errorf("VisibleEventNIDsBetween: wrong number of rooms, want 3 got %+v", roomIDToVisibleRange)
}
2021-10-21 18:27:19 +01:00
// check that we can query subsets too
roomIDToVisibleRangesSubset, err := store.visibleEventNIDsBetweenForRooms(alice, []string{roomA, roomB}, startPos, latestPos)
2021-10-21 18:27:19 +01:00
if err != nil {
t.Fatalf("VisibleEventNIDsBetweenForRooms to %d: %s", latestPos, err)
}
if len(roomIDToVisibleRangesSubset) != 2 {
t.Errorf("VisibleEventNIDsBetweenForRooms: wrong number of rooms, want 2 got %+v", roomIDToVisibleRange)
2021-10-21 18:27:19 +01:00
}
// For Room A: from=1, to=10, returns { RoomA: [ [1,10] ]} (tests events in joined room)
verifyRange(t, roomIDToVisibleRange, roomA, [2]int64{
1 + startPos, 10 + startPos,
})
// For Room B: from=1, to=10, returns { RoomB: [ [5,10] ]} (tests joining a room starts events)
verifyRange(t, roomIDToVisibleRange, roomB, [2]int64{
5 + startPos, 10 + startPos,
})
// For Room C: from=1, to=10, returns { RoomC: [ [0,9] ]} (tests leaving a room stops events)
// We start at 0 because it's the earliest event (we were joined since the beginning of the room state)
verifyRange(t, roomIDToVisibleRange, roomC, [2]int64{
0 + startPos, 9 + startPos,
})
// change the users else we will still have some rooms from A,B,C present if the user is still joined
// to those rooms.
alice = "@aliceDE:localhost"
bob = "@bobDE:localhost"
// Stream Positions
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// Room D Maj E Mal E Maj E Mal E
// Room E E Mai E E Maj E E
timelineInjections = []struct {
RoomID string
Events []json.RawMessage
}{
{
RoomID: roomD,
Events: []json.RawMessage{
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, alice),
},
},
{
RoomID: roomE,
Events: []json.RawMessage{
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
testutils.NewStateEvent(t, "m.room.member", alice, bob, map[string]interface{}{"membership": "invite"}),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp.Add(1*time.Second))),
},
},
{
RoomID: roomD,
Events: []json.RawMessage{
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
testutils.NewStateEvent(t, "m.room.member", alice, alice, map[string]interface{}{"membership": "leave"}),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, alice),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp.Add(1*time.Second))),
testutils.NewStateEvent(t, "m.room.member", alice, alice, map[string]interface{}{"membership": "leave"}),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp.Add(1*time.Second))),
},
},
{
RoomID: roomE,
Events: []json.RawMessage{
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, alice),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
testutils.NewEvent(t, "m.room.message", bob, map[string]interface{}{}, testutils.WithTimestamp(baseTimestamp)),
},
},
}
startPos, err = store.LatestEventNID()
if err != nil {
t.Fatalf("LatestEventNID: %s", err)
}
for _, tl := range timelineInjections {
2023-09-19 12:41:25 +01:00
accResult, err := store.Accumulate(userID, tl.RoomID, sync2.TimelineResponse{Events: tl.Events})
if err != nil {
t.Fatalf("Accumulate on %s failed: %s", tl.RoomID, err)
}
2023-09-07 19:21:44 +01:00
t.Logf("%s added %d new events", tl.RoomID, accResult.NumNew)
}
latestPos, err = store.LatestEventNID()
if err != nil {
t.Fatalf("LatestEventNID: %s", err)
}
t.Logf("DE Start=%d Latest=%d", startPos, latestPos)
roomIDToVisibleRange, err = store.VisibleEventNIDsBetween(alice, startPos, latestPos)
if err != nil {
t.Fatalf("VisibleEventNIDsBetween to %d: %s", latestPos, err)
}
for roomID, r := range roomIDToVisibleRange {
t.Logf("%v => [%d,%d]", roomID, r[0]-startPos, r[1]-startPos)
}
if len(roomIDToVisibleRange) != 2 {
t.Errorf("VisibleEventNIDsBetween: wrong number of rooms, want 2 got %+v", roomIDToVisibleRange)
}
// For Room D: from=1, to=15 returns { RoomD: [ 8,10 ] } (tests multi-join/leave)
verifyRange(t, roomIDToVisibleRange, roomD, [2]int64{
8 + startPos, 10 + startPos,
})
// For Room E: from=1, to=15 returns { RoomE: [ 13,15 ] } (tests invites)
verifyRange(t, roomIDToVisibleRange, roomE, [2]int64{
13 + startPos, 15 + startPos,
})
}
func TestStorageLatestEventsInRoomsPrevBatch(t *testing.T) {
store := NewStorage(postgresConnectionString)
defer store.Teardown()
roomID := "!joined:bar"
alice := "@alice_TestStorageLatestEventsInRoomsPrevBatch:localhost"
stateEvents := []json.RawMessage{
testutils.NewStateEvent(t, "m.room.create", "", alice, map[string]interface{}{"creator": alice}),
2022-07-12 15:12:02 +01:00
testutils.NewJoinEvent(t, alice),
}
timelines := []struct {
timeline []json.RawMessage
prevBatch string
}{
{
prevBatch: "batch A",
timeline: []json.RawMessage{
testutils.NewEvent(t, "m.room.message", alice, map[string]interface{}{"body": "1"}), // prev batch should be associated with this event
testutils.NewEvent(t, "m.room.message", alice, map[string]interface{}{"body": "2"}),
testutils.NewEvent(t, "m.room.message", alice, map[string]interface{}{"body": "3"}),
},
},
{
prevBatch: "batch B",
timeline: []json.RawMessage{
testutils.NewEvent(t, "m.room.message", alice, map[string]interface{}{"body": "4"}),
},
},
{
prevBatch: "batch C",
timeline: []json.RawMessage{
testutils.NewEvent(t, "m.room.message", alice, map[string]interface{}{"body": "5"}),
testutils.NewEvent(t, "m.room.message", alice, map[string]interface{}{"body": "6"}),
},
},
}
2023-04-17 18:21:58 +01:00
_, err := store.Initialise(roomID, stateEvents)
if err != nil {
t.Fatalf("failed to initialise: %s", err)
}
eventIDs := []string{}
for _, timeline := range timelines {
2023-09-19 12:41:25 +01:00
_, err = store.Accumulate(userID, roomID, sync2.TimelineResponse{Events: timeline.timeline, PrevBatch: timeline.prevBatch})
if err != nil {
t.Fatalf("failed to accumulate: %s", err)
}
for _, ev := range timeline.timeline {
eventIDs = append(eventIDs, gjson.ParseBytes(ev).Get("event_id").Str)
}
}
t.Logf("events: %v", eventIDs)
var idsToNIDs map[string]int64
sqlutil.WithTransaction(store.EventsTable.db, func(txn *sqlx.Tx) error {
idsToNIDs, err = store.EventsTable.SelectNIDsByIDs(txn, eventIDs)
if err != nil {
t.Fatalf("failed to get nids for events: %s", err)
}
return nil
})
t.Logf("nids: %v", idsToNIDs)
wantPrevBatches := []string{
// first chunk
timelines[0].prevBatch,
timelines[1].prevBatch,
timelines[1].prevBatch,
// second chunk
timelines[1].prevBatch,
// third chunk
timelines[2].prevBatch,
"",
}
for i := range wantPrevBatches {
wantPrevBatch := wantPrevBatches[i]
eventNID := idsToNIDs[eventIDs[i]]
// closest batch to the last event in the chunk (latest nid) is always the next prev batch token
var pb string
_ = sqlutil.WithTransaction(store.DB, func(txn *sqlx.Tx) (err error) {
pb, err = store.EventsTable.SelectClosestPrevBatch(txn, roomID, eventNID)
if err != nil {
t.Fatalf("failed to SelectClosestPrevBatch: %s", err)
}
return nil
})
if pb != wantPrevBatch {
t.Fatalf("SelectClosestPrevBatch: got %v want %v", pb, wantPrevBatch)
}
}
}
func TestGlobalSnapshot(t *testing.T) {
alice := "@TestGlobalSnapshot_alice:localhost"
bob := "@TestGlobalSnapshot_bob:localhost"
roomAlice := "!alice"
roomBob := "!bob"
roomAliceBob := "!alicebob"
roomSpace := "!space"
oldRoomID := "!old"
newRoomID := "!new"
roomType := "room_type_here"
spaceRoomType := "m.space"
roomIDToEventMap := map[string][]json.RawMessage{
roomAlice: {
testutils.NewStateEvent(t, "m.room.create", "", alice, map[string]interface{}{"creator": alice, "predecessor": map[string]string{
"room_id": oldRoomID,
"event_id": "$something",
}}),
testutils.NewJoinEvent(t, alice),
testutils.NewStateEvent(t, "m.room.encryption", "", alice, map[string]interface{}{"algorithm": "m.megolm.v1.aes-sha2"}),
},
roomBob: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob, "type": roomType}),
testutils.NewJoinEvent(t, bob),
testutils.NewStateEvent(t, "m.room.name", "", alice, map[string]interface{}{"name": "My Room"}),
},
roomAliceBob: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob}),
testutils.NewJoinEvent(t, bob),
testutils.NewJoinEvent(t, alice),
testutils.NewStateEvent(t, "m.room.canonical_alias", "", alice, map[string]interface{}{"alias": "#alias"}),
testutils.NewStateEvent(t, "m.room.tombstone", "", alice, map[string]interface{}{"replacement_room": newRoomID, "body": "yep"}),
},
roomSpace: {
testutils.NewStateEvent(t, "m.room.create", "", bob, map[string]interface{}{"creator": bob, "type": spaceRoomType}),
testutils.NewJoinEvent(t, bob),
testutils.NewStateEvent(t, "m.space.child", newRoomID, bob, map[string]interface{}{"via": []string{"somewhere"}}),
testutils.NewStateEvent(t, "m.space.child", "!no_via", bob, map[string]interface{}{}),
testutils.NewStateEvent(t, "m.room.member", alice, bob, map[string]interface{}{"membership": "invite"}),
},
}
2023-06-05 18:10:35 +01:00
if err := cleanDB(t); err != nil {
t.Fatalf("failed to wipe DB: %s", err)
}
2023-06-05 18:10:35 +01:00
store := NewStorage(postgresConnectionString)
defer store.Teardown()
for roomID, stateEvents := range roomIDToEventMap {
2023-04-17 18:21:58 +01:00
_, err := store.Initialise(roomID, stateEvents)
assertNoError(t, err)
}
snapshot, err := store.GlobalSnapshot()
assertNoError(t, err)
wantJoinedMembers := map[string][]string{
roomAlice: {alice},
roomBob: {bob},
roomAliceBob: {bob, alice}, // user IDs are ordered by event nid, and bob joined first so he is first
roomSpace: {bob},
}
if !reflect.DeepEqual(snapshot.AllJoinedMembers, wantJoinedMembers) {
t.Errorf("Snapshot.AllJoinedMembers:\ngot: %+v\nwant: %+v", snapshot.AllJoinedMembers, wantJoinedMembers)
}
wantMetadata := map[string]internal.RoomMetadata{
roomAlice: {
RoomID: roomAlice,
JoinCount: 1,
LastMessageTimestamp: gjson.ParseBytes(roomIDToEventMap[roomAlice][len(roomIDToEventMap[roomAlice])-1]).Get("origin_server_ts").Uint(),
Heroes: []internal.Hero{{ID: alice}},
Encrypted: true,
PredecessorRoomID: &oldRoomID,
2023-05-12 10:11:25 +01:00
ChildSpaceRooms: make(map[string]struct{}),
},
roomBob: {
RoomID: roomBob,
JoinCount: 1,
LastMessageTimestamp: gjson.ParseBytes(roomIDToEventMap[roomBob][len(roomIDToEventMap[roomBob])-1]).Get("origin_server_ts").Uint(),
Heroes: []internal.Hero{{ID: bob}},
NameEvent: "My Room",
RoomType: &roomType,
2023-05-12 10:11:25 +01:00
ChildSpaceRooms: make(map[string]struct{}),
},
roomAliceBob: {
RoomID: roomAliceBob,
JoinCount: 2,
LastMessageTimestamp: gjson.ParseBytes(roomIDToEventMap[roomAliceBob][len(roomIDToEventMap[roomAliceBob])-1]).Get("origin_server_ts").Uint(),
Heroes: []internal.Hero{{ID: bob}, {ID: alice}},
CanonicalAlias: "#alias",
UpgradedRoomID: &newRoomID,
2023-05-12 10:11:25 +01:00
ChildSpaceRooms: make(map[string]struct{}),
},
roomSpace: {
RoomID: roomSpace,
JoinCount: 1,
InviteCount: 1,
LastMessageTimestamp: gjson.ParseBytes(roomIDToEventMap[roomSpace][len(roomIDToEventMap[roomSpace])-1]).Get("origin_server_ts").Uint(),
Heroes: []internal.Hero{{ID: bob}, {ID: alice}},
RoomType: &spaceRoomType,
ChildSpaceRooms: map[string]struct{}{
newRoomID: {},
},
},
}
for roomID, want := range wantMetadata {
assertRoomMetadata(t, snapshot.GlobalMetadata[roomID], want)
}
}
func TestAllJoinedMembers(t *testing.T) {
assertNoError(t, cleanDB(t))
2023-07-17 15:55:10 +01:00
store := NewStorage(postgresConnectionString)
defer store.Teardown()
alice := "@alice:localhost"
bob := "@bob:localhost"
charlie := "@charlie:localhost"
doris := "@doris:localhost"
eve := "@eve:localhost"
frank := "@frank:localhost"
2023-07-17 16:25:28 +01:00
// Alice is always the creator and the inviter for simplicity's sake
2023-07-17 15:55:10 +01:00
testCases := []struct {
Name string
InitMemberships [][2]string
AccumulateMemberships [][2]string
2023-07-17 16:25:28 +01:00
RoomID string // tests set this dynamically
2023-07-17 15:55:10 +01:00
WantJoined []string
WantInvited []string
}{
{
Name: "basic joined users",
InitMemberships: [][2]string{{alice, "join"}},
AccumulateMemberships: [][2]string{{bob, "join"}},
WantJoined: []string{alice, bob},
},
{
Name: "basic invited users",
2023-07-17 16:25:28 +01:00
InitMemberships: [][2]string{{alice, "join"}, {charlie, "invite"}},
2023-07-17 15:55:10 +01:00
AccumulateMemberships: [][2]string{{bob, "invite"}},
WantJoined: []string{alice},
2023-07-17 16:25:28 +01:00
WantInvited: []string{bob, charlie},
2023-07-17 15:55:10 +01:00
},
{
Name: "many join/leaves, use latest",
InitMemberships: [][2]string{{alice, "join"}, {charlie, "join"}, {frank, "join"}},
AccumulateMemberships: [][2]string{{bob, "join"}, {charlie, "leave"}, {frank, "leave"}, {charlie, "join"}, {eve, "join"}},
WantJoined: []string{alice, bob, charlie, eve},
},
{
Name: "many invites, use latest",
InitMemberships: [][2]string{{alice, "join"}, {doris, "join"}},
AccumulateMemberships: [][2]string{{doris, "leave"}, {charlie, "invite"}, {doris, "invite"}},
WantJoined: []string{alice},
WantInvited: []string{charlie, doris},
},
{
Name: "invite and rejection in accumulate",
InitMemberships: [][2]string{{alice, "join"}},
AccumulateMemberships: [][2]string{{frank, "invite"}, {frank, "leave"}},
WantJoined: []string{alice},
},
{
Name: "invite in initial, rejection in accumulate",
InitMemberships: [][2]string{{alice, "join"}, {frank, "invite"}},
AccumulateMemberships: [][2]string{{frank, "leave"}},
WantJoined: []string{alice},
},
}
2023-07-17 16:25:28 +01:00
serialise := func(memberships [][2]string) []json.RawMessage {
var result []json.RawMessage
for _, userWithMembership := range memberships {
target := userWithMembership[0]
sender := userWithMembership[0]
membership := userWithMembership[1]
if membership == "invite" {
// Alice is always the inviter
sender = alice
}
result = append(result, testutils.NewStateEvent(t, "m.room.member", target, sender, map[string]interface{}{
"membership": membership,
}))
}
return result
2023-07-17 15:55:10 +01:00
}
2023-07-17 16:25:28 +01:00
for i, tc := range testCases {
roomID := fmt.Sprintf("!TestAllJoinedMembers_%d:localhost", i)
_, err := store.Initialise(roomID, append([]json.RawMessage{
testutils.NewStateEvent(t, "m.room.create", "", alice, map[string]interface{}{
"creator": alice, // alice is always the creator
}),
}, serialise(tc.InitMemberships)...))
2023-07-17 15:55:10 +01:00
assertNoError(t, err)
2023-07-17 16:25:28 +01:00
2023-09-19 12:41:25 +01:00
_, err = store.Accumulate(userID, roomID, sync2.TimelineResponse{
2023-09-13 12:46:57 +01:00
Events: serialise(tc.AccumulateMemberships),
PrevBatch: "foo",
})
2023-07-17 16:25:28 +01:00
assertNoError(t, err)
testCases[i].RoomID = roomID // remember this for later
2023-07-17 15:55:10 +01:00
}
// should get all joined members correctly
var joinedMembers map[string][]string
// should set join/invite counts correctly
var roomMetadatas map[string]internal.RoomMetadata
err := sqlutil.WithTransaction(store.DB, func(txn *sqlx.Tx) error {
tableName, err := store.PrepareSnapshot(txn)
if err != nil {
return err
}
joinedMembers, roomMetadatas, err = store.AllJoinedMembers(txn, tableName)
return err
})
assertNoError(t, err)
2023-07-17 16:25:28 +01:00
for _, tc := range testCases {
roomID := tc.RoomID
if roomID == "" {
t.Fatalf("test case has no room id set: %+v", tc)
}
// make sure joined members match
sort.Strings(joinedMembers[roomID])
sort.Strings(tc.WantJoined)
if !reflect.DeepEqual(joinedMembers[roomID], tc.WantJoined) {
t.Errorf("%v: got joined members %v want %v", tc.Name, joinedMembers[roomID], tc.WantJoined)
}
// make sure join/invite counts match
wantJoined := len(tc.WantJoined)
wantInvited := len(tc.WantInvited)
metadata, ok := roomMetadatas[roomID]
if !ok {
t.Fatalf("no room metadata for room %v", roomID)
}
if metadata.InviteCount != wantInvited {
t.Errorf("%v: got invite count %d want %d", tc.Name, metadata.InviteCount, wantInvited)
}
if metadata.JoinCount != wantJoined {
t.Errorf("%v: got join count %d want %d", tc.Name, metadata.JoinCount, wantJoined)
}
}
2023-07-17 15:55:10 +01:00
}
func TestCircularSlice(t *testing.T) {
testCases := []struct {
name string
max int
appends []int64
want []int64 // these get sorted in the test
}{
{
name: "wraparound",
max: 5,
appends: []int64{9, 8, 7, 6, 5, 4, 3, 2},
want: []int64{2, 3, 4, 5, 6},
},
{
name: "exact",
max: 5,
appends: []int64{9, 8, 7, 6, 5},
want: []int64{5, 6, 7, 8, 9},
},
{
name: "unfilled",
max: 5,
appends: []int64{9, 8, 7},
want: []int64{7, 8, 9},
},
{
name: "wraparound x2",
max: 5,
appends: []int64{9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 10},
want: []int64{0, 1, 2, 3, 10},
},
}
for _, tc := range testCases {
2023-09-08 18:16:38 +01:00
cs := &circularSlice[int64]{
max: tc.max,
}
for _, val := range tc.appends {
cs.append(val)
}
sort.Slice(cs.vals, func(i, j int) bool {
return cs.vals[i] < cs.vals[j]
})
if !reflect.DeepEqual(cs.vals, tc.want) {
t.Errorf("%s: got %v want %v", tc.name, cs.vals, tc.want)
}
}
}
func TestStorage_FetchMemberships(t *testing.T) {
assertNoError(t, cleanDB(t))
store := NewStorage(postgresConnectionString)
defer store.Teardown()
events := []json.RawMessage{
testutils.NewStateEvent(t, "m.room.create", "", "@alice:test", map[string]any{}),
testutils.NewStateEvent(t, "m.room.member", "@alice:test", "@alice:test", map[string]any{"membership": "join"}),
testutils.NewStateEvent(t, "m.room.member", "@brian:test", "@alice:test", map[string]any{"membership": "invite"}),
testutils.NewStateEvent(t, "m.room.member", "@chris:test", "@chris:test", map[string]any{"membership": "leave"}),
testutils.NewStateEvent(t, "m.room.member", "@david:test", "@alice:test", map[string]any{"membership": "ban"}),
testutils.NewStateEvent(t, "m.room.member", "@erika:test", "@erika:test", map[string]any{"membership": "join"}),
testutils.NewStateEvent(t, "m.room.member", "@frank:test", "@erika:test", map[string]any{"membership": "invite"}),
testutils.NewStateEvent(t, "m.room.member", "@glory:test", "@glory:test", map[string]any{"membership": "leave"}),
testutils.NewStateEvent(t, "m.room.member", "@helen:test", "@alice:test", map[string]any{"membership": "ban"}),
}
const roomID = "!unimportant"
err := sqlutil.WithTransaction(store.DB, func(txn *sqlx.Tx) (err error) {
_, err = store.Accumulator.Initialise(roomID, events)
return err
})
assertNoError(t, err)
joins, invites, leaves, err := store.FetchMemberships(roomID)
assertNoError(t, err)
// Do not assume an order from the DB.
sort.Slice(joins, func(i, j int) bool {
return joins[i] < joins[j]
})
sort.Slice(invites, func(i, j int) bool {
return invites[i] < invites[j]
})
sort.Slice(leaves, func(i, j int) bool {
return leaves[i] < leaves[j]
})
assertValue(t, "joins", joins, []string{"@alice:test", "@erika:test"})
assertValue(t, "invites", invites, []string{"@brian:test", "@frank:test"})
assertValue(t, "joins", leaves, []string{"@chris:test", "@david:test", "@glory:test", "@helen:test"})
}
type persistOpts struct {
withInitialEvents bool
numTimelineEvents int
ofWhichNumState int
}
func mustPersistEvents(t *testing.T, roomID string, store *Storage, opts persistOpts) {
t.Helper()
var events []json.RawMessage
if opts.withInitialEvents {
events = createInitialEvents(t, userID)
}
numAddedStateEvents := 0
for i := 0; i < opts.numTimelineEvents; i++ {
var ev json.RawMessage
if numAddedStateEvents < opts.ofWhichNumState {
numAddedStateEvents++
ev = testutils.NewStateEvent(t, "some_kind_of_state", fmt.Sprintf("%d", rand.Int63()), userID, map[string]interface{}{
"num": numAddedStateEvents,
})
} else {
ev = testutils.NewEvent(t, "some_kind_of_message", userID, map[string]interface{}{
"msg": "yep",
})
}
events = append(events, ev)
}
mustAccumulate(t, store, roomID, events)
}
func mustAccumulate(t *testing.T, store *Storage, roomID string, events []json.RawMessage) {
t.Helper()
_, err := store.Accumulate(userID, roomID, sync2.TimelineResponse{
Events: events,
})
if err != nil {
t.Fatalf("Failed to accumulate: %s", err)
}
}
func mustHaveNumSnapshots(t *testing.T, db *sqlx.DB, roomID string, numSnapshots int) {
t.Helper()
var val int
err := db.QueryRow(`SELECT count(*) FROM syncv3_snapshots WHERE room_id=$1`, roomID).Scan(&val)
if err != nil {
t.Fatalf("mustHaveNumSnapshots: %s", err)
}
if val != numSnapshots {
t.Fatalf("mustHaveNumSnapshots: got %d want %d snapshots", val, numSnapshots)
}
}
func mustNotError(t *testing.T, err error) {
t.Helper()
if err == nil {
return
}
t.Fatalf("err: %s", err)
}
func TestRemoveInaccessibleStateSnapshots(t *testing.T) {
store := NewStorage(postgresConnectionString)
store.MaxTimelineLimit = 50 // we nuke if we have >50+1 snapshots
roomOnlyMessages := "!TestRemoveInaccessibleStateSnapshots_roomOnlyMessages:localhost"
mustPersistEvents(t, roomOnlyMessages, store, persistOpts{
withInitialEvents: true,
numTimelineEvents: 100,
ofWhichNumState: 0,
})
roomOnlyState := "!TestRemoveInaccessibleStateSnapshots_roomOnlyState:localhost"
mustPersistEvents(t, roomOnlyState, store, persistOpts{
withInitialEvents: true,
numTimelineEvents: 100,
ofWhichNumState: 100,
})
roomPartialStateAndMessages := "!TestRemoveInaccessibleStateSnapshots_roomPartialStateAndMessages:localhost"
mustPersistEvents(t, roomPartialStateAndMessages, store, persistOpts{
withInitialEvents: true,
numTimelineEvents: 100,
ofWhichNumState: 30,
})
roomOverwriteState := "TestRemoveInaccessibleStateSnapshots_roomOverwriteState:localhost"
mustPersistEvents(t, roomOverwriteState, store, persistOpts{
withInitialEvents: true,
})
mustAccumulate(t, store, roomOverwriteState, []json.RawMessage{testutils.NewStateEvent(t, "overwrite", "", userID, map[string]interface{}{"val": 1})})
mustAccumulate(t, store, roomOverwriteState, []json.RawMessage{testutils.NewStateEvent(t, "overwrite", "", userID, map[string]interface{}{"val": 2})})
mustHaveNumSnapshots(t, store.DB, roomOnlyMessages, 4) // initial state only, one for each state event
mustHaveNumSnapshots(t, store.DB, roomOnlyState, 104) // initial state + 100 state events
mustHaveNumSnapshots(t, store.DB, roomPartialStateAndMessages, 34) // initial state + 30 state events
mustHaveNumSnapshots(t, store.DB, roomOverwriteState, 6) // initial state + 2 overwrite state events
mustNotError(t, store.RemoveInaccessibleStateSnapshots())
mustHaveNumSnapshots(t, store.DB, roomOnlyMessages, 4) // it should not be touched as 4 < 51
mustHaveNumSnapshots(t, store.DB, roomOnlyState, 51) // it should be capped at 51
mustHaveNumSnapshots(t, store.DB, roomPartialStateAndMessages, 34) // it should not be touched as 34 < 51
mustHaveNumSnapshots(t, store.DB, roomOverwriteState, 6) // it should not be touched as 6 < 51
// calling it again does nothing
mustNotError(t, store.RemoveInaccessibleStateSnapshots())
mustHaveNumSnapshots(t, store.DB, roomOnlyMessages, 4)
mustHaveNumSnapshots(t, store.DB, roomOnlyState, 51)
mustHaveNumSnapshots(t, store.DB, roomPartialStateAndMessages, 34)
mustHaveNumSnapshots(t, store.DB, roomOverwriteState, 6) // it should not be touched as 6 < 51
// adding one extra state snapshot to each room and repeating RemoveInaccessibleStateSnapshots
mustPersistEvents(t, roomOnlyMessages, store, persistOpts{numTimelineEvents: 1, ofWhichNumState: 1})
mustPersistEvents(t, roomOnlyState, store, persistOpts{numTimelineEvents: 1, ofWhichNumState: 1})
mustPersistEvents(t, roomPartialStateAndMessages, store, persistOpts{numTimelineEvents: 1, ofWhichNumState: 1})
mustNotError(t, store.RemoveInaccessibleStateSnapshots())
mustHaveNumSnapshots(t, store.DB, roomOnlyMessages, 5)
mustHaveNumSnapshots(t, store.DB, roomOnlyState, 51) // still capped
mustHaveNumSnapshots(t, store.DB, roomPartialStateAndMessages, 35)
// adding 51 timeline events and repeating RemoveInaccessibleStateSnapshots does nothing
mustPersistEvents(t, roomOnlyMessages, store, persistOpts{numTimelineEvents: 51})
mustPersistEvents(t, roomOnlyState, store, persistOpts{numTimelineEvents: 51})
mustPersistEvents(t, roomPartialStateAndMessages, store, persistOpts{numTimelineEvents: 51})
mustNotError(t, store.RemoveInaccessibleStateSnapshots())
mustHaveNumSnapshots(t, store.DB, roomOnlyMessages, 5)
mustHaveNumSnapshots(t, store.DB, roomOnlyState, 51)
mustHaveNumSnapshots(t, store.DB, roomPartialStateAndMessages, 35)
// overwrite 52 times and check the current state is 52 (shows we are keeping the right snapshots)
for i := 0; i < 52; i++ {
mustAccumulate(t, store, roomOverwriteState, []json.RawMessage{testutils.NewStateEvent(t, "overwrite", "", userID, map[string]interface{}{"val": 1 + i})})
}
mustHaveNumSnapshots(t, store.DB, roomOverwriteState, 58)
mustNotError(t, store.RemoveInaccessibleStateSnapshots())
mustHaveNumSnapshots(t, store.DB, roomOverwriteState, 51)
roomsTable := NewRoomsTable(store.DB)
mustNotError(t, sqlutil.WithTransaction(store.DB, func(txn *sqlx.Tx) error {
snapID, err := roomsTable.CurrentAfterSnapshotID(txn, roomOverwriteState)
if err != nil {
return err
}
state, err := store.StateSnapshot(snapID)
if err != nil {
return err
}
// find the 'overwrite' event and make sure the val is 52
for _, ev := range state {
evv := gjson.ParseBytes(ev)
if evv.Get("type").Str != "overwrite" {
continue
}
if evv.Get("content.val").Int() != 52 {
return fmt.Errorf("val for overwrite state event was not 52: %v", evv.Raw)
}
}
return nil
}))
}
func createInitialEvents(t *testing.T, creator string) []json.RawMessage {
t.Helper()
baseTimestamp := time.Now()
var pl gomatrixserverlib.PowerLevelContent
pl.Defaults()
pl.Users = map[string]int64{
creator: 100,
}
// all with the same timestamp as they get made atomically
return []json.RawMessage{
testutils.NewStateEvent(t, "m.room.create", "", creator, map[string]interface{}{"creator": creator}, testutils.WithTimestamp(baseTimestamp)),
testutils.NewJoinEvent(t, creator, testutils.WithTimestamp(baseTimestamp)),
testutils.NewStateEvent(t, "m.room.power_levels", "", creator, pl, testutils.WithTimestamp(baseTimestamp)),
testutils.NewStateEvent(t, "m.room.join_rules", "", creator, map[string]interface{}{"join_rule": "public"}, testutils.WithTimestamp(baseTimestamp)),
}
}
2023-06-05 18:10:35 +01:00
func cleanDB(t *testing.T) error {
// make a fresh DB which is unpolluted from other tests
db, close := connectToDB(t)
_, err := db.Exec(`
DROP TABLE IF EXISTS syncv3_rooms;
DROP TABLE IF EXISTS syncv3_invites;
DROP TABLE IF EXISTS syncv3_snapshots;
DROP TABLE IF EXISTS syncv3_spaces;`)
close()
return err
}
func assertRoomMetadata(t *testing.T, got, want internal.RoomMetadata) {
t.Helper()
assertValue(t, "CanonicalAlias", got.CanonicalAlias, want.CanonicalAlias)
assertValue(t, "ChildSpaceRooms", got.ChildSpaceRooms, want.ChildSpaceRooms)
assertValue(t, "Encrypted", got.Encrypted, want.Encrypted)
assertValue(t, "Heroes", sortHeroes(got.Heroes), sortHeroes(want.Heroes))
assertValue(t, "InviteCount", got.InviteCount, want.InviteCount)
assertValue(t, "JoinCount", got.JoinCount, want.JoinCount)
assertValue(t, "LastMessageTimestamp", got.LastMessageTimestamp, want.LastMessageTimestamp)
assertValue(t, "NameEvent", got.NameEvent, want.NameEvent)
assertValue(t, "PredecessorRoomID", got.PredecessorRoomID, want.PredecessorRoomID)
assertValue(t, "RoomID", got.RoomID, want.RoomID)
assertValue(t, "RoomType", got.RoomType, want.RoomType)
assertValue(t, "TypingEvent", got.TypingEvent, want.TypingEvent)
assertValue(t, "UpgradedRoomID", got.UpgradedRoomID, want.UpgradedRoomID)
}
func assertValue(t *testing.T, msg string, got, want interface{}) {
2023-09-13 14:12:18 +01:00
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("%s: got %v want %v", msg, got, want)
}
}
func sortHeroes(heroes []internal.Hero) []internal.Hero {
sort.Slice(heroes, func(i, j int) bool {
return heroes[i].ID < heroes[j].ID
})
return heroes
}
func verifyRange(t *testing.T, result map[string][2]int64, roomID string, wantRange [2]int64) {
t.Helper()
gotRange := result[roomID]
if gotRange == [2]int64{} {
t.Fatalf("no range was returned for room %s", roomID)
}
if !reflect.DeepEqual(gotRange, wantRange) {
t.Errorf("%s range got %v want %v", roomID, gotRange, wantRange)
}
}