Skip to content

Commit

Permalink
Partial test
Browse files Browse the repository at this point in the history
  • Loading branch information
kegsay committed Nov 17, 2023
1 parent b8832d6 commit 3a2e9ef
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 27 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ There is an exhaustive set of tests that this repository aims to exercise which
Membership ACLs:
- [x] Happy case Alice and Bob in an encrypted room can send and receive encrypted messages, and decrypt them all.
- [x] Bob can see messages when he was invited but not joined to the room. Subsequent messages are also decryptable.
- [ ] In a public, `shared` history visibility room, a new user Bob cannot decrypt earlier messages prior to his join, despite being able to see the events. Subsequent messages are decryptable.
- [x] In a public, `shared` history visibility room, a new user Bob cannot decrypt earlier messages prior to his join, despite being able to see the events. Subsequent messages are decryptable.
- [ ] Bob leaves the room. Some messages are sent. Bob rejoins and cannot decrypt the messages sent whilst he was gone (ensuring we cycle keys). Repeat this again with a device instead of a user (so 2x device, 1 remains always in the room, 1 then logs out -> messages sent -> logs in again).
- [ ] Alice invites Bob, Bob changes their device, then Bob joins. Bob should be able to see Alice's message.

Expand Down
26 changes: 15 additions & 11 deletions internal/api/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ func NewJSClient(t *testing.T, opts ClientCreationOpts) (Client, error) {

// any events need to log the control string so we get notified
chrome.MustExecute(t, ctx, fmt.Sprintf(`window.__client.on("Event.decrypted", function(event) {
if (event.getType() !== "m.room.message") {
return; // only use messages
}
console.log("%s"+event.getRoomId()+"||"+JSON.stringify(event.getEffectiveEvent()));
});`, CONSOLE_LOG_CONTROL_STRING))
chrome.MustExecute(t, ctx, fmt.Sprintf(`window.__client.on("event", function(event) {
console.log("%s"+event.getRoomId()+"||"+JSON.stringify(event.getEffectiveEvent()));
});`, CONSOLE_LOG_CONTROL_STRING))

Expand All @@ -181,30 +181,28 @@ func NewJSClient(t *testing.T, opts ClientCreationOpts) (Client, error) {
func (c *JSClient) Close(t *testing.T) {
c.cancel()
c.listeners = make(map[int32]func(roomID string, ev Event))
if t.Failed() {
// print logs for this test

}
}

func (c *JSClient) UserID() string {
return c.userID
}

func (c *JSClient) MustGetEvent(t *testing.T, roomID, eventID string) Event {
t.Helper()
// serialised output (if encrypted):
// {
// encrypted: { event }
// decrypted: { event }
// }
// else just returns { event }
evSerialised := chrome.MustExecuteInto[string](t, c.ctx, fmt.Sprintf(`
JSON.stringify(window.__client.getRoom("%s")?.getLiveTimeline()?.getEvents().filter((ev) => {
JSON.stringify(window.__client.getRoom("%s")?.getLiveTimeline()?.getEvents().filter((ev, i) => {
console.log("MustGetEvent["+i+"] => " + ev.getId()+ " " + JSON.stringify(ev.toJSON()));
return ev.getId() === "%s";
})[0].toJSON());
`, roomID, eventID))
if !gjson.Valid(evSerialised) {
fatalf(t, "MustGetEvent(%s, %s): invalid event, got %s", roomID, eventID, evSerialised)
fatalf(t, "MustGetEvent(%s, %s) %s (js): invalid event, got %s", roomID, eventID, c.userID, evSerialised)
}
result := gjson.Parse(evSerialised)
decryptedEvent := result.Get("decrypted")
Expand Down Expand Up @@ -232,6 +230,7 @@ func (c *JSClient) MustGetEvent(t *testing.T, roomID, eventID string) Event {
// StartSyncing to begin syncing from sync v2 / sliding sync.
// Tests should call stopSyncing() at the end of the test.
func (c *JSClient) StartSyncing(t *testing.T) (stopSyncing func()) {
t.Helper()
t.Logf("%s is starting to sync", c.userID)
chrome.MustExecute(t, c.ctx, fmt.Sprintf(`
var fn;
Expand Down Expand Up @@ -266,6 +265,7 @@ func (c *JSClient) StartSyncing(t *testing.T) (stopSyncing func()) {
// IsRoomEncrypted returns true if the room is encrypted. May return an error e.g if you
// provide a bogus room ID.
func (c *JSClient) IsRoomEncrypted(t *testing.T, roomID string) (bool, error) {
t.Helper()
isEncrypted, err := chrome.ExecuteInto[bool](
t, c.ctx, fmt.Sprintf(`window.__client.isRoomEncrypted("%s")`, roomID),
)
Expand All @@ -278,6 +278,7 @@ func (c *JSClient) IsRoomEncrypted(t *testing.T, roomID string) (bool, error) {
// SendMessage sends the given text as an m.room.message with msgtype:m.text into the given
// room.
func (c *JSClient) SendMessage(t *testing.T, roomID, text string) (eventID string) {
t.Helper()
res, err := chrome.AwaitExecuteInto[map[string]interface{}](t, c.ctx, fmt.Sprintf(`window.__client.sendMessage("%s", {
"msgtype": "m.text",
"body": "%s"
Expand All @@ -287,12 +288,14 @@ func (c *JSClient) SendMessage(t *testing.T, roomID, text string) (eventID strin
}

func (c *JSClient) MustBackpaginate(t *testing.T, roomID string, count int) {
t.Helper()
chrome.MustAwaitExecute(t, c.ctx, fmt.Sprintf(
`window.__client.scrollback(window.__client.getRoom("%s"), %d);`, roomID, count,
))
}

func (c *JSClient) WaitUntilEventInRoom(t *testing.T, roomID string, checker func(e Event) bool) Waiter {
t.Helper()
return &jsTimelineWaiter{
roomID: roomID,
checker: checker,
Expand Down Expand Up @@ -326,6 +329,7 @@ type jsTimelineWaiter struct {
}

func (w *jsTimelineWaiter) Wait(t *testing.T, s time.Duration) {
t.Helper()
updates := make(chan bool, 3)
cancel := w.client.listenForUpdates(func(roomID string, ev Event) {
if w.roomID != roomID {
Expand All @@ -349,11 +353,11 @@ func (w *jsTimelineWaiter) Wait(t *testing.T, s time.Duration) {
for {
timeLeft := s - time.Since(start)
if timeLeft <= 0 {
fatalf(t, "%s: Wait[%s]: timed out", w.client.userID, w.roomID)
fatalf(t, "%s (js): Wait[%s]: timed out", w.client.userID, w.roomID)
}
select {
case <-time.After(timeLeft):
fatalf(t, "%s: Wait[%s]: timed out", w.client.userID, w.roomID)
fatalf(t, "%s (js): Wait[%s]: timed out", w.client.userID, w.roomID)
case <-updates:
return
}
Expand Down
42 changes: 27 additions & 15 deletions internal/api/rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ func init() {
var zero uint32

type RustRoomInfo struct {
attachedListener bool
room *matrix_sdk_ffi.Room
timeline []*Event
stream *matrix_sdk_ffi.TaskHandle
room *matrix_sdk_ffi.Room
timeline []*Event
}

type RustClient struct {
Expand Down Expand Up @@ -73,14 +73,15 @@ func (c *RustClient) Close(t *testing.T) {
}

func (c *RustClient) MustGetEvent(t *testing.T, roomID, eventID string) Event {
t.Helper()
room := c.findRoom(t, roomID)
timelineItem, err := room.GetEventTimelineItemByEventId(eventID)
if err != nil {
fatalf(t, "MustGetEvent(%s, %s): %s", roomID, eventID, err)
fatalf(t, "MustGetEvent(rust) %s (%s, %s): %s", c.userID, roomID, eventID, err)
}
ev := eventTimelineItemToEvent(timelineItem)
if ev == nil {
fatalf(t, "MustGetEvent(%s, %s): found timeline item but failed to convert it to an Event", roomID, eventID)
fatalf(t, "MustGetEvent(rust) %s (%s, %s): found timeline item but failed to convert it to an Event", c.userID, roomID, eventID)
}
return *ev
}
Expand Down Expand Up @@ -179,7 +180,7 @@ func (c *RustClient) SendMessage(t *testing.T, roomID, text string) (eventID str
r.Send(matrix_sdk_ffi.MessageEventContentFromHtml(text, text))
select {
case <-time.After(5 * time.Second):
fatalf(t, "SendMessage: timed out after 5s")
fatalf(t, "SendMessage(rust) %s: timed out after 5s", c.userID)
case <-ch:
return
}
Expand Down Expand Up @@ -212,6 +213,7 @@ func (c *RustClient) findRoomInMap(roomID string) *matrix_sdk_ffi.Room {

// findRoom returns the room, waiting up to 5s for it to appear
func (c *RustClient) findRoom(t *testing.T, roomID string) *matrix_sdk_ffi.Room {
t.Helper()
room := c.findRoomInMap(roomID)
if room != nil {
return room
Expand Down Expand Up @@ -258,18 +260,20 @@ func (c *RustClient) Logf(t *testing.T, format string, args ...interface{}) {
}

func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ffi.Room {
t.Helper()
r := c.findRoom(t, roomID)
must.NotEqual(t, r, nil, fmt.Sprintf("room %s does not exist", roomID))

info := c.rooms[roomID]
if info.attachedListener {
if info.stream != nil {
return r
}

c.Logf(t, "[%s]AddTimelineListener[%s]", c.userID, roomID)
// we need a timeline listener before we can send messages
result := r.AddTimelineListener(&timelineListener{fn: func(diff []*matrix_sdk_ffi.TimelineDiff) {
timeline := c.rooms[roomID].timeline
var newEvents []*Event
c.Logf(t, "[%s]AddTimelineListener[%s] TimelineDiff len=%d", c.userID, roomID, len(diff))
for _, d := range diff {
switch d.Change() {
Expand All @@ -285,6 +289,7 @@ func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ff
}
timeline = slices.Insert(timeline, i, timelineItemToEvent(insertData.Item))
fmt.Printf("[%s]_______ INSERT %+v\n", c.userID, timeline[i])
newEvents = append(newEvents, timeline[i])
case matrix_sdk_ffi.TimelineChangeAppend:
appendItems := d.Append()
if appendItems == nil {
Expand All @@ -294,6 +299,7 @@ func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ff
ev := timelineItemToEvent(item)
timeline = append(timeline, ev)
fmt.Printf("[%s]_______ APPEND %+v\n", c.userID, ev)
newEvents = append(newEvents, ev)
}
case matrix_sdk_ffi.TimelineChangePushBack: // append but 1 element
pbData := d.PushBack()
Expand All @@ -303,6 +309,7 @@ func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ff
ev := timelineItemToEvent(*pbData)
timeline = append(timeline, ev)
fmt.Printf("[%s]_______ PUSH BACK %+v\n", c.userID, ev)
newEvents = append(newEvents, ev)
case matrix_sdk_ffi.TimelineChangeSet:
setData := d.Set()
if setData == nil {
Expand All @@ -315,6 +322,7 @@ func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ff
}
timeline[i] = timelineItemToEvent(setData.Item)
fmt.Printf("[%s]_______ SET %+v\n", c.userID, timeline[i])
newEvents = append(newEvents, timeline[i])
default:
t.Logf("Unhandled TimelineDiff change %v", d.Change())
}
Expand All @@ -323,19 +331,22 @@ func (c *RustClient) ensureListening(t *testing.T, roomID string) *matrix_sdk_ff
for _, l := range c.listeners {
l(roomID)
}
for _, e := range newEvents {
c.Logf(t, "TimelineDiff change: %+v", e)
}
}})
events := make([]*Event, len(result.Items))
for i := range result.Items {
events[i] = timelineItemToEvent(result.Items[i])
}
c.rooms[roomID].stream = result.ItemsStream
c.rooms[roomID].timeline = events
c.Logf(t, "[%s]AddTimelineListener[%s] result.Items len=%d", c.userID, roomID, len(result.Items))
if len(events) > 0 {
for _, l := range c.listeners {
l(roomID)
}
}
info.attachedListener = true
return r
}

Expand Down Expand Up @@ -381,12 +392,15 @@ func (w *timelineWaiter) Wait(t *testing.T, s time.Duration) {
return
}

updates := make(chan bool, 10)
updates := make(chan bool, 3)
cancel := w.client.listenForUpdates(func(roomID string) {
if w.roomID != roomID {
return
}
updates <- true
if !checkForEvent() {
return
}
close(updates)
})
defer cancel()

Expand All @@ -395,15 +409,13 @@ func (w *timelineWaiter) Wait(t *testing.T, s time.Duration) {
for {
timeLeft := s - time.Since(start)
if timeLeft <= 0 {
fatalf(t, "%s: Wait[%s]: timed out", w.client.userID, w.roomID)
fatalf(t, "%s (rust): Wait[%s]: timed out", w.client.userID, w.roomID)
}
select {
case <-time.After(timeLeft):
fatalf(t, "%s: Wait[%s]: timed out", w.client.userID, w.roomID)
fatalf(t, "%s (rust): Wait[%s]: timed out", w.client.userID, w.roomID)
case <-updates:
if checkForEvent() {
return
}
return
}
}
}
Expand Down
89 changes: 89 additions & 0 deletions tests/membership_acls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,92 @@ func TestBobCanSeeButNotDecryptHistoryInPublicRoom(t *testing.T) {
must.Equal(t, ev.FailedToDecrypt, true, "message not marked as failed to decrypt")
})
}

// Bob leaves the room. Some messages are sent. Bob rejoins and cannot decrypt the messages sent whilst he was gone (ensuring we cycle keys).
func TestOnRejoinBobCanSeeButNotDecryptHistoryInPublicRoom(t *testing.T) {
ClientTypeMatrix(t, func(t *testing.T, clientTypeA, clientTypeB api.ClientType) {
// Setup Code
// ----------
deployment := Deploy(t)
// pre-register alice and bob
csapiAlice := deployment.Register(t, "hs1", helpers.RegistrationOpts{
LocalpartSuffix: "alice",
Password: "complement-crypto-password",
})
csapiBob := deployment.Register(t, "hs1", helpers.RegistrationOpts{
LocalpartSuffix: "bob",
Password: "complement-crypto-password",
})
roomID := csapiAlice.MustCreateRoom(t, map[string]interface{}{
"name": "TestOnRejoinBobCanSeeButNotDecryptHistoryInPublicRoom",
"preset": "public_chat", // shared history visibility
"initial_state": []map[string]interface{}{
{
"type": "m.room.encryption",
"state_key": "",
"content": map[string]interface{}{
"algorithm": "m.megolm.v1.aes-sha2",
},
},
},
})
csapiBob.MustJoinRoom(t, roomID, []string{"hs1"})
ss := deployment.SlidingSyncURL(t)

// SDK testing below
// -----------------

// login both clients first, so OTKs etc are uploaded.
alice := MustLoginClient(t, clientTypeA, api.FromComplementClient(csapiAlice, "complement-crypto-password"), ss)
defer alice.Close(t)
bob := MustLoginClient(t, clientTypeB, api.FromComplementClient(csapiBob, "complement-crypto-password"), ss)
defer bob.Close(t)

// Alice and Bob start syncing. Both are in the same room
aliceStopSyncing := alice.StartSyncing(t)
defer aliceStopSyncing()
bobStopSyncing := bob.StartSyncing(t)
defer bobStopSyncing()

// Alice sends a message which Bob should be able to decrypt.
bothJoinedBody := "Alice and Bob in a room"
waiter := bob.WaitUntilEventInRoom(t, roomID, api.CheckEventHasBody(bothJoinedBody))
evID := alice.SendMessage(t, roomID, bothJoinedBody)
t.Logf("bob (%s) waiting for event %s", bob.Type(), evID)
waiter.Wait(t, 5*time.Second)

// now bob leaves the room, wait for alice to see it
waiter = alice.WaitUntilEventInRoom(t, roomID, api.CheckEventHasMembership(bob.UserID(), "leave"))
csapiBob.MustLeaveRoom(t, roomID)
waiter.Wait(t, 5*time.Second)

// now alice sends another message, which should use a key that bob does not have. Wait for the remote echo to come back.
onlyAliceBody := "Only me on my lonesome"
waiter = alice.WaitUntilEventInRoom(t, roomID, api.CheckEventHasBody(onlyAliceBody))
evID = alice.SendMessage(t, roomID, onlyAliceBody)
t.Logf("alice (%s) waiting for event %s", alice.Type(), evID)
waiter.Wait(t, 5*time.Second)

// now bob rejoins the room, wait until he sees it.
csapiBob.MustJoinRoom(t, roomID, []string{"hs1"})
waiter = bob.WaitUntilEventInRoom(t, roomID, api.CheckEventHasMembership(bob.UserID(), "join"))
waiter.Wait(t, 5*time.Second)
// this is required for some reason else tests fail
time.Sleep(time.Second)

// bob hits scrollback and should see but not be able to decrypt the message
bob.MustBackpaginate(t, roomID, 5)
ev := bob.MustGetEvent(t, roomID, evID)
must.NotEqual(t, ev.Text, onlyAliceBody, "bob was able to decrypt a message from before he was joined")
must.Equal(t, ev.FailedToDecrypt, true, "message not marked as failed to decrypt")

/* TODO: needs client changes
time.Sleep(time.Second) // let alice realise bob is back in the room
// bob should be able to decrypt subsequent messages
bothJoinedBody = "Alice and Bob in a room again"
evID = alice.SendMessage(t, roomID, bothJoinedBody)
time.Sleep(time.Second) // TODO: use a Waiter; currently this is broken as it seems like listeners get detached on leave?
ev = bob.MustGetEvent(t, roomID, evID)
must.Equal(t, ev.Text, bothJoinedBody, "event was not decrypted correctly") */
})
}

0 comments on commit 3a2e9ef

Please sign in to comment.