diff --git a/internal/chrome/exec.go b/internal/api/js/chrome/chrome.go similarity index 55% rename from internal/chrome/exec.go rename to internal/api/js/chrome/chrome.go index e0ecc57..f580860 100644 --- a/internal/chrome/exec.go +++ b/internal/api/js/chrome/chrome.go @@ -1,3 +1,7 @@ +// package chrome provides helper functions to execute JS in a Chrome browser +// +// This would ordinarily be done via a Chrome struct but Go does not allow +// generic methods, only generic static functions, producing "method must have no type parameters". package chrome import ( @@ -6,9 +10,42 @@ import ( "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" + "github.com/matrix-org/complement-crypto/internal/api" "github.com/matrix-org/complement/must" ) +// Void is a type which can be used when you want to run an async function without returning anything. +// It can stop large responses causing errors "Object reference chain is too long (-32000)" +// when we don't care about the response. +type Void *runtime.RemoteObject + +// Run an anonymous async iffe in the browser. Set the type parameter to a basic data type +// which can be returned as JSON e.g string, map[string]any, []string. If you do not want +// to return anything, use chrome.Void +func RunAsyncFn[T any](t *testing.T, ctx context.Context, js string) (*T, error) { + t.Helper() + out := new(T) + err := chromedp.Run(ctx, + chromedp.Evaluate(`(async () => {`+js+`})()`, &out, func(p *runtime.EvaluateParams) *runtime.EvaluateParams { + return p.WithAwaitPromise(true) + }), + ) + if err != nil { + return nil, err + } + return out, nil +} + +// MustRunAsyncFn is RunAsyncFn but fails the test if an error is returned when executing. +func MustRunAsyncFn[T any](t *testing.T, ctx context.Context, js string) *T { + t.Helper() + result, err := RunAsyncFn[T](t, ctx, js) + if err != nil { + api.Fatalf(t, "MustRunAsyncFn: %s", err) + } + return result +} + func MustExecuteInto[T any](t *testing.T, ctx context.Context, js string) T { t.Helper() out, err := ExecuteInto[T](t, ctx, js) diff --git a/internal/api/js/js.go b/internal/api/js/js.go index 91c2cd7..f717569 100644 --- a/internal/api/js/js.go +++ b/internal/api/js/js.go @@ -19,7 +19,7 @@ import ( "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" "github.com/matrix-org/complement-crypto/internal/api" - "github.com/matrix-org/complement-crypto/internal/chrome" + "github.com/matrix-org/complement-crypto/internal/api/js/chrome" "github.com/matrix-org/complement/must" "github.com/tidwall/gjson" ) @@ -336,7 +336,8 @@ func (c *JSClient) MustBackpaginate(t *testing.T, roomID string, count int) { } func (c *JSClient) MustBackupKeys(t *testing.T) (recoveryKey string) { - key, err := chrome.AwaitExecuteInto[string](t, c.ctx, `(async () => { + t.Helper() + key := chrome.MustRunAsyncFn[string](t, c.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 @@ -347,11 +348,7 @@ func (c *JSClient) MustBackupKeys(t *testing.T) (recoveryKey string) { }); // now we can enable key backups await window.__client.getCrypto().checkKeyBackupAndEnable(); - return recoveryKey.encodedPrivateKey; - })()`) - if err != nil { - api.Fatalf(t, "MustBackupKeys: %s", err) - } + return recoveryKey.encodedPrivateKey;`) // 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.. @@ -360,7 +357,7 @@ func (c *JSClient) MustBackupKeys(t *testing.T) (recoveryKey string) { } func (c *JSClient) MustLoadBackup(t *testing.T, recoveryKey string) { - chrome.MustAwaitExecute(t, c.ctx, fmt.Sprintf(`(async () => { + chrome.MustRunAsyncFn[chrome.Void](t, c.ctx, fmt.Sprintf(` // we assume the recovery key is the private key for the default key id so // figure out what that key id is. const keyId = await window.__client.secretStorage.getDefaultKeyId(); @@ -374,8 +371,8 @@ func (c *JSClient) MustLoadBackup(t *testing.T, recoveryKey string) { console.log("key backup: ", JSON.stringify(keyBackupCheck)); // FIXME: this just doesn't seem to work, causing 'Error: getSecretStorageKey callback returned invalid data' because the key ID // cannot be found... - await window.__client.restoreKeyBackupWithSecretStorage(keyBackupCheck ? keyBackupCheck.backupInfo : null, undefined, undefined); - })()`, recoveryKey)) + await window.__client.restoreKeyBackupWithSecretStorage(keyBackupCheck ? keyBackupCheck.backupInfo : null, undefined, undefined);`, + recoveryKey)) } func (c *JSClient) WaitUntilEventInRoom(t *testing.T, roomID string, checker func(e api.Event) bool) api.Waiter { diff --git a/internal/chrome/callbacks.go b/internal/chrome/callbacks.go deleted file mode 100644 index 7b44280..0000000 --- a/internal/chrome/callbacks.go +++ /dev/null @@ -1 +0,0 @@ -package chrome diff --git a/tests/addons/README.md b/tests/addons/README.md new file mode 100644 index 0000000..0550e01 --- /dev/null +++ b/tests/addons/README.md @@ -0,0 +1,37 @@ +### mitmproxy + +This directory contains code that will be used as a [mitmproxy addon](https://docs.mitmproxy.org/stable/addons-overview/). + +How this works: + - A vanilla `mitmproxy` is run in the same network as the homeservers. + - It is told to proxy both hs1 and hs2 i.e `mitmdump --mode reverse:http://hs1:8008@3000` + - It is also told to run a normal proxy, to which a Flask HTTP server is attached. + - The Flask HTTP server can be used to control mitmproxy at test runtime. This is done via the Controller HTTP API. + + +### Controller HTTP API + +**This is highly experimental and will change without warning.** + +`mitmproxy` is run once for all tests. To avoid test pollution, the controller is "locked" for the duration +of a test and must be "unlocked" afterwards. When acquiring the lock, options can be set on `mitmproxy`. + +``` +POST /options/lock + { + "options": { + "body_size_limit": "3m", + } + } + HTTP/1.1 200 OK + { + "reset_id": "some_opaque_string" + } +``` + +``` +POST /options/unlock +{ + "reset_id": "some_opaque_string" +} +``` \ No newline at end of file