Skip to content

Commit

Permalink
Merge pull request #146 from matrix-org/kegan/rm-must-api
Browse files Browse the repository at this point in the history
Split the Client API in two
  • Loading branch information
kegsay authored Oct 2, 2024
2 parents 119bf5e + d630e0e commit 87719af
Show file tree
Hide file tree
Showing 17 changed files with 337 additions and 372 deletions.
165 changes: 110 additions & 55 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ type Client interface {
// uploaded to the server. Failure to block will result in flakey tests as other users may not
// encrypt for this Client due to not detecting keys for the Client.
Login(t ct.TestLike, opts ClientCreationOpts) error
// MustStartSyncing to begin syncing from sync v2 / sliding sync.
// Tests should call stopSyncing() at the end of the test.
// MUST BLOCK until the initial sync is complete.
// Fails the test if there was a problem syncing.
MustStartSyncing(t ct.TestLike) (stopSyncing func())
// StartSyncing to begin syncing from sync v2 / sliding sync.
// Tests should call stopSyncing() at the end of the test.
// MUST BLOCK until the initial sync is complete.
Expand All @@ -50,20 +45,18 @@ type Client interface {
InviteUser(t ct.TestLike, roomID, userID string) error
// SendMessage sends the given text as an encrypted/unencrypted message in the room, depending
// if the room is encrypted or not. Returns the event ID of the sent event, so MUST BLOCK until the event has been sent.
SendMessage(t ct.TestLike, roomID, text string) (eventID string)
// TrySendMessage tries to send the message, but can fail.
TrySendMessage(t ct.TestLike, roomID, text string) (eventID string, err error)
// If the event cannot be sent, returns an error.
SendMessage(t ct.TestLike, roomID, text string) (eventID string, err error)
// Wait until an event is seen in the given room. The checker functions can be custom or you can use
// a pre-defined one like api.CheckEventHasMembership, api.CheckEventHasBody, or api.CheckEventHasEventID.
WaitUntilEventInRoom(t ct.TestLike, roomID string, checker func(e Event) bool) Waiter
// Backpaginate in this room by `count` events.
MustBackpaginate(t ct.TestLike, roomID string, count int)
// MustGetEvent will return the client's view of this event, or fail the test if the event cannot be found.
MustGetEvent(t ct.TestLike, roomID, eventID string) Event
// MustBackupKeys will backup E2EE keys, else fail the test.
MustBackupKeys(t ct.TestLike) (recoveryKey string)
// MustLoadBackup will recover E2EE keys from the latest backup, else fail the test.
MustLoadBackup(t ct.TestLike, recoveryKey string)
// Backpaginate in this room by `count` events. Returns an error if there was a problem backpaginating.
// Getting to the beginning of the room is not an error condition.
Backpaginate(t ct.TestLike, roomID string, count int) error
// GetEvent will return the client's view of this event, or returns an error if the event cannot be found.
GetEvent(t ct.TestLike, roomID, eventID string) (*Event, error)
// BackupKeys will backup E2EE keys, else return an error.
BackupKeys(t ct.TestLike) (recoveryKey string, err error)
// LoadBackup will recover E2EE keys from the latest backup, else return an error.
LoadBackup(t ct.TestLike, recoveryKey string) error
// GetNotification gets push notification-like information for the given event. If there is a problem, an error is returned.
Expand Down Expand Up @@ -96,9 +89,86 @@ type Client interface {
Opts() ClientCreationOpts
}

type Notification struct {
Event
HasMentions *bool
// TestClient is a Client with extra helper functions added to make writing tests easier.
// Client implementations are not expected to implement these helper functions, and are
// instead composed together by the test rig itself.
type TestClient interface {
Client
// MustStartSyncing is StartSyncing but fails the test on error.
MustStartSyncing(t ct.TestLike) (stopSyncing func())
// MustLoadBackup is LoadBackup but fails the test on error.
MustLoadBackup(t ct.TestLike, recoveryKey string)
// MustSendMessage is SendMessage but fails the test on error.
MustSendMessage(t ct.TestLike, roomID, text string) (eventID string)
// MustGetEvent is GetEvent but fails the test on error.
MustGetEvent(t ct.TestLike, roomID, eventID string) *Event
// MustBackupKeys is BackupKeys but fails the test on error.
MustBackupKeys(t ct.TestLike) (recoveryKey string)
// MustBackpaginate is Backpaginate but fails the test on error.
MustBackpaginate(t ct.TestLike, roomID string, count int)
}

// NewTestClient wraps a Client implementation with helper functions which tests can use.
func NewTestClient(c Client) TestClient {
return &testClientImpl{
Client: c,
}
}

type testClientImpl struct {
Client
}

func (c *testClientImpl) MustStartSyncing(t ct.TestLike) (stopSyncing func()) {
t.Helper()
stopSyncing, err := c.StartSyncing(t)
if err != nil {
ct.Fatalf(t, "MustStartSyncing: %s", err)
}
return stopSyncing
}

func (c *testClientImpl) MustLoadBackup(t ct.TestLike, recoveryKey string) {
t.Helper()
err := c.LoadBackup(t, recoveryKey)
if err != nil {
ct.Fatalf(t, "MustLoadBackup: %s", err)
}
}

func (c *testClientImpl) MustBackupKeys(t ct.TestLike) (recoveryKey string) {
t.Helper()
recoveryKey, err := c.BackupKeys(t)
if err != nil {
ct.Fatalf(t, "MustBackupKeys: %s", err)
}
return recoveryKey
}

func (c *testClientImpl) MustBackpaginate(t ct.TestLike, roomID string, count int) {
t.Helper()
err := c.Backpaginate(t, roomID, count)
if err != nil {
ct.Fatalf(t, "MustBackpaginate: %s", err)
}
}

func (c *testClientImpl) MustSendMessage(t ct.TestLike, roomID, text string) (eventID string) {
t.Helper()
eventID, err := c.SendMessage(t, roomID, text)
if err != nil {
ct.Fatalf(t, "MustSendMessage: %s", err)
}
return eventID
}

func (c *testClientImpl) MustGetEvent(t ct.TestLike, roomID, eventID string) *Event {
t.Helper()
ev, err := c.GetEvent(t, roomID, eventID)
if err != nil {
ct.Fatalf(t, "MustGetEvent: %s", err)
}
return ev
}

type LoggedClient struct {
Expand Down Expand Up @@ -130,18 +200,10 @@ func (c *LoggedClient) ForceClose(t ct.TestLike) {
c.Client.ForceClose(t)
}

func (c *LoggedClient) MustGetEvent(t ct.TestLike, roomID, eventID string) Event {
func (c *LoggedClient) GetEvent(t ct.TestLike, roomID, eventID string) (*Event, error) {
t.Helper()
c.Logf(t, "%s MustGetEvent(%s, %s)", c.logPrefix(), roomID, eventID)
return c.Client.MustGetEvent(t, roomID, eventID)
}

func (c *LoggedClient) MustStartSyncing(t ct.TestLike) (stopSyncing func()) {
t.Helper()
c.Logf(t, "%s MustStartSyncing starting to sync", c.logPrefix())
stopSyncing = c.Client.MustStartSyncing(t)
c.Logf(t, "%s MustStartSyncing now syncing", c.logPrefix())
return
c.Logf(t, "%s GetEvent(%s, %s)", c.logPrefix(), roomID, eventID)
return c.Client.GetEvent(t, roomID, eventID)
}

func (c *LoggedClient) StartSyncing(t ct.TestLike) (stopSyncing func(), err error) {
Expand All @@ -158,19 +220,11 @@ func (c *LoggedClient) IsRoomEncrypted(t ct.TestLike, roomID string) (bool, erro
return c.Client.IsRoomEncrypted(t, roomID)
}

func (c *LoggedClient) TrySendMessage(t ct.TestLike, roomID, text string) (eventID string, err error) {
t.Helper()
c.Logf(t, "%s TrySendMessage %s => %s", c.logPrefix(), roomID, text)
eventID, err = c.Client.TrySendMessage(t, roomID, text)
c.Logf(t, "%s TrySendMessage %s => %s", c.logPrefix(), roomID, eventID)
return
}

func (c *LoggedClient) SendMessage(t ct.TestLike, roomID, text string) (eventID string) {
func (c *LoggedClient) SendMessage(t ct.TestLike, roomID, text string) (eventID string, err error) {
t.Helper()
c.Logf(t, "%s SendMessage %s => %s", c.logPrefix(), roomID, text)
eventID = c.Client.SendMessage(t, roomID, text)
c.Logf(t, "%s SendMessage %s => %s", c.logPrefix(), roomID, eventID)
eventID, err = c.Client.SendMessage(t, roomID, text)
c.Logf(t, "%s SendMessage %s => %s %s", c.logPrefix(), roomID, eventID, err)
return
}

Expand All @@ -180,24 +234,20 @@ func (c *LoggedClient) WaitUntilEventInRoom(t ct.TestLike, roomID string, checke
return c.Client.WaitUntilEventInRoom(t, roomID, checker)
}

func (c *LoggedClient) MustBackpaginate(t ct.TestLike, roomID string, count int) {
func (c *LoggedClient) Backpaginate(t ct.TestLike, roomID string, count int) error {
t.Helper()
c.Logf(t, "%s MustBackpaginate %d %s", c.logPrefix(), count, roomID)
c.Client.MustBackpaginate(t, roomID, count)
c.Logf(t, "%s Backpaginate %d %s", c.logPrefix(), count, roomID)
err := c.Client.Backpaginate(t, roomID, count)
c.Logf(t, "%s Backpaginate %d %s => %s", c.logPrefix(), count, roomID, err)
return err
}

func (c *LoggedClient) MustBackupKeys(t ct.TestLike) (recoveryKey string) {
func (c *LoggedClient) BackupKeys(t ct.TestLike) (recoveryKey string, err error) {
t.Helper()
c.Logf(t, "%s MustBackupKeys", c.logPrefix())
recoveryKey = c.Client.MustBackupKeys(t)
c.Logf(t, "%s MustBackupKeys => %s", c.logPrefix(), recoveryKey)
return recoveryKey
}

func (c *LoggedClient) MustLoadBackup(t ct.TestLike, recoveryKey string) {
t.Helper()
c.Logf(t, "%s MustLoadBackup key=%s", c.logPrefix(), recoveryKey)
c.Client.MustLoadBackup(t, recoveryKey)
c.Logf(t, "%s BackupKeys", c.logPrefix())
recoveryKey, err = c.Client.BackupKeys(t)
c.Logf(t, "%s BackupKeys => %s %s", c.logPrefix(), recoveryKey, err)
return recoveryKey, err
}

func (c *LoggedClient) LoadBackup(t ct.TestLike, recoveryKey string) error {
Expand All @@ -216,6 +266,11 @@ func (c *LoggedClient) logPrefix() string {
return fmt.Sprintf("[%s](%s)", c.UserID(), c.Type())
}

type Notification struct {
Event
HasMentions *bool
}

// ClientCreationOpts are options to use when creating crypto clients.
//
// This contains a mixture of generic options which can be used across any client, and specific
Expand Down
53 changes: 20 additions & 33 deletions internal/api/js/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/matrix-org/complement-crypto/internal/api"
"github.com/matrix-org/complement-crypto/internal/api/js/chrome"
"github.com/matrix-org/complement/ct"
"github.com/matrix-org/complement/must"
"github.com/tidwall/gjson"
)

Expand Down Expand Up @@ -509,22 +508,25 @@ func (c *JSClient) InviteUser(t ct.TestLike, roomID, userID string) error {
return err
}

func (c *JSClient) MustGetEvent(t ct.TestLike, roomID, eventID string) api.Event {
func (c *JSClient) GetEvent(t ct.TestLike, roomID, eventID string) (*api.Event, error) {
t.Helper()
// serialised output (if encrypted):
// {
// encrypted: { event }
// decrypted: { event }
// }
// else just returns { event }
evSerialised := chrome.MustRunAsyncFn[string](t, c.browser.Ctx, fmt.Sprintf(`
evSerialised, err := chrome.RunAsyncFn[string](t, c.browser.Ctx, fmt.Sprintf(`
return 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 err != nil {
return nil, fmt.Errorf("failed to get event %s: %s", eventID, err)
}
if !gjson.Valid(*evSerialised) {
ct.Fatalf(t, "MustGetEvent(%s, %s) %s (js): invalid event, got %s", roomID, eventID, c.userID, *evSerialised)
return nil, fmt.Errorf("invalid event %s, got %s", eventID, *evSerialised)
}
result := gjson.Parse(*evSerialised)
decryptedEvent := result.Get("decrypted")
Expand All @@ -533,7 +535,7 @@ func (c *JSClient) MustGetEvent(t ct.TestLike, roomID, eventID string) api.Event
}
encryptedEvent := result.Get("encrypted")
//fmt.Printf("DECRYPTED: %s\nENCRYPTED: %s\n\n", decryptedEvent.Raw, encryptedEvent.Raw)
ev := api.Event{
ev := &api.Event{
ID: decryptedEvent.Get("event_id").Str,
Text: decryptedEvent.Get("content.body").Str,
Sender: decryptedEvent.Get("sender").Str,
Expand All @@ -546,14 +548,7 @@ func (c *JSClient) MustGetEvent(t ct.TestLike, roomID, eventID string) api.Event
ev.FailedToDecrypt = true
}

return ev
}

func (c *JSClient) MustStartSyncing(t ct.TestLike) (stopSyncing func()) {
t.Helper()
stopSyncing, err := c.StartSyncing(t)
must.NotError(t, "StartSyncing", err)
return stopSyncing
return ev, nil
}

// StartSyncing to begin syncing from sync v2 / sliding sync.
Expand Down Expand Up @@ -609,16 +604,7 @@ func (c *JSClient) IsRoomEncrypted(t ct.TestLike, roomID string) (bool, error) {
return *isEncrypted, nil
}

// SendMessage sends the given text as an m.room.message with msgtype:m.text into the given
// room.
func (c *JSClient) SendMessage(t ct.TestLike, roomID, text string) (eventID string) {
t.Helper()
eventID, err := c.TrySendMessage(t, roomID, text)
must.NotError(t, "failed to sendMessage", err)
return eventID
}

func (c *JSClient) TrySendMessage(t ct.TestLike, roomID, text string) (eventID string, err error) {
func (c *JSClient) SendMessage(t ct.TestLike, roomID, text string) (eventID string, err error) {
t.Helper()
res, err := chrome.RunAsyncFn[map[string]interface{}](t, c.browser.Ctx, fmt.Sprintf(`
return await window.__client.sendMessage("%s", {
Expand All @@ -631,16 +617,17 @@ func (c *JSClient) TrySendMessage(t ct.TestLike, roomID, text string) (eventID s
return (*res)["event_id"].(string), nil
}

func (c *JSClient) MustBackpaginate(t ct.TestLike, roomID string, count int) {
func (c *JSClient) Backpaginate(t ct.TestLike, roomID string, count int) error {
t.Helper()
chrome.MustRunAsyncFn[chrome.Void](t, c.browser.Ctx, fmt.Sprintf(
_, err := chrome.RunAsyncFn[chrome.Void](t, c.browser.Ctx, fmt.Sprintf(
`await window.__client.scrollback(window.__client.getRoom("%s"), %d);`, roomID, count,
))
return err
}

func (c *JSClient) MustBackupKeys(t ct.TestLike) (recoveryKey string) {
func (c *JSClient) BackupKeys(t ct.TestLike) (recoveryKey string, err error) {
t.Helper()
key := chrome.MustRunAsyncFn[string](t, c.browser.Ctx, `
key, err := chrome.RunAsyncFn[string](t, c.browser.Ctx, `
// we need to ensure that we have a recovery key first, though we don't actually care about it..?
const recoveryKey = await window.__client.getCrypto().createRecoveryKeyFromPassphrase();
// now use said key to make backups
Expand All @@ -652,15 +639,14 @@ func (c *JSClient) MustBackupKeys(t ct.TestLike) (recoveryKey string) {
// now we can enable key backups
await window.__client.getCrypto().checkKeyBackupAndEnable();
return recoveryKey.encodedPrivateKey;`)
if err != nil {
return "", fmt.Errorf("error enabling key backup: %s", err)
}
// the backup loop which sends keys will wait between 0-10s before uploading keys...
// See https://github.com/matrix-org/matrix-js-sdk/blob/49624d5d7308e772ebee84322886a39d2e866869/src/rust-crypto/backup.ts#L319
// Ideally this would be configurable..
time.Sleep(11 * time.Second)
return *key
}

func (c *JSClient) MustLoadBackup(t ct.TestLike, recoveryKey string) {
must.NotError(t, "failed to load backup", c.LoadBackup(t, recoveryKey))
return *key, nil
}

func (c *JSClient) LoadBackup(t ct.TestLike, recoveryKey string) error {
Expand Down Expand Up @@ -693,8 +679,9 @@ func (c *JSClient) WaitUntilEventInRoom(t ct.TestLike, roomID string, checker fu
func (c *JSClient) Logf(t ct.TestLike, format string, args ...interface{}) {
t.Helper()
formatted := fmt.Sprintf(t.Name()+": "+format, args...)
firstLine := strings.Split(formatted, "\n")[0]
if c.browser.Ctx.Err() == nil { // don't log on dead browsers
chrome.MustRunAsyncFn[chrome.Void](t, c.browser.Ctx, fmt.Sprintf(`console.log("%s");`, strings.Replace(formatted, `"`, `\"`, -1)))
chrome.MustRunAsyncFn[chrome.Void](t, c.browser.Ctx, fmt.Sprintf(`console.log("%s");`, strings.Replace(firstLine, `"`, `\"`, -1)))
t.Logf(format, args...)
}
}
Expand Down
Loading

0 comments on commit 87719af

Please sign in to comment.