Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split the Client API in two #146

Merged
merged 5 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading