diff --git a/.gitignore b/.gitignore index 1d44abe..2116329 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ js-sdk/node_modules js-sdk/dist -tests/dist \ No newline at end of file +internal/api/dist diff --git a/internal/api/client.go b/internal/api/client.go index 3a4ad08..df18ec7 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -7,25 +7,35 @@ import ( "github.com/matrix-org/complement/client" ) +type ClientType string + +var ( + ClientTypeRust ClientType = "rust" + ClientTypeJS ClientType = "js" +) + // Client represents a generic crypto client. // It is an abstraction to allow tests to interact with JS and FFI bindings in an agnostic way. type Client interface { - // Init is called prior to any test execution. Do any setup code here e.g run a browser. - // Call close() when the test terminates to clean up resources. - // TODO: will this be too slow if we spin up a browser for each test? - Init(t *testing.T) (close func()) + // Close is called to clean up resources. + // Specifically, we need to shut off existing browsers and any FFI bindings. + // If we get callbacks/events after this point, tests may panic if the callbacks + // log messages. + Close(t *testing.T) // StartSyncing to begin syncing from sync v2 / sliding sync. // Tests should call stopSyncing() at the end of the test. 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. - IsRoomEncrypted(roomID string) (bool, error) + 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. Returns the event ID of the sent event. SendMessage(t *testing.T, roomID, text string) // Wait until an event with the given body is seen. Not all impls expose event IDs // hence needing to use body as a proxy. WaitUntilEventInRoom(t *testing.T, roomID, wantBody string) Waiter + + Type() ClientType } // ClientCreationOpts are generic opts to use when creating crypto clients. diff --git a/internal/api/js.go b/internal/api/js.go index 3bf23a1..7619797 100644 --- a/internal/api/js.go +++ b/internal/api/js.go @@ -1,40 +1,248 @@ package api import ( + "context" + "embed" + "encoding/json" + "fmt" + "io/fs" + "log" + "net" + "net/http" + "strconv" + "strings" + "sync" + "sync/atomic" "testing" + "time" + + "github.com/chromedp/cdproto/runtime" + "github.com/chromedp/chromedp" + "github.com/matrix-org/complement-crypto/internal/chrome" + "github.com/matrix-org/complement/must" ) -type JSClient struct{} +const CONSOLE_LOG_CONTROL_STRING = "CC:" // for "complement-crypto" + +//go:embed dist +var jsSDKDistDirectory embed.FS -func NewJSClient(opts ClientCreationOpts) Client { - return nil +type JSClient struct { + ctx context.Context + cancel func() + baseJSURL string + listeners map[int32]func(roomID, text string) + listenerID atomic.Int32 + userID string } -// Init is called prior to any test execution. Do any setup code here e.g run a browser. -// Call close() when the test terminates to clean up resources. -// TODO: will this be too slow if we spin up a browser for each test? -func (c *JSClient) Init(t *testing.T) (close func()) { - return +func NewJSClient(t *testing.T, opts ClientCreationOpts) (Client, error) { + // start a headless chrome + ctx, cancel := chromedp.NewContext(context.Background(), chromedp.WithBrowserOption( + chromedp.WithBrowserLogf(log.Printf), chromedp.WithBrowserErrorf(log.Printf), //chromedp.WithBrowserDebugf(log.Printf), + )) + jsc := &JSClient{ + listeners: make(map[int32]func(roomID, text string)), + userID: opts.UserID, + } + // Listen for console logs for debugging AND to communicate live updates + chromedp.ListenTarget(ctx, func(ev interface{}) { + switch ev := ev.(type) { + case *runtime.EventConsoleAPICalled: + for _, arg := range ev.Args { + s, err := strconv.Unquote(string(arg.Value)) + if err != nil { + log.Println(err) + continue + } + // TODO: debug mode only? + fmt.Printf("[%s] console.log %s\n", opts.UserID, s) + + if strings.HasPrefix(s, CONSOLE_LOG_CONTROL_STRING) { + val := strings.TrimPrefix(s, CONSOLE_LOG_CONTROL_STRING) + // for now the format is always 'room_id||text' + segs := strings.Split(val, "||") + for _, l := range jsc.listeners { + l(segs[0], segs[1]) + } + } + } + } + }) + + // strip /dist so /index.html loads correctly as does /assets/xxx.js + c, err := fs.Sub(jsSDKDistDirectory, "dist") + if err != nil { + return nil, fmt.Errorf("failed to strip /dist off JS SDK files: %s", err) + } + + baseJSURL := "" + // run js-sdk (need to run this as a web server to avoid CORS errors you'd otherwise get with file: URLs) + var wg sync.WaitGroup + wg.Add(1) + mux := &http.ServeMux{} + mux.Handle("/", http.FileServer(http.FS(c))) + startServer := func() { + srv := &http.Server{ + Addr: "127.0.0.1:0", + Handler: mux, + } + ln, err := net.Listen("tcp", srv.Addr) + if err != nil { + panic(err) + } + baseJSURL = "http://" + ln.Addr().String() + fmt.Println("JS SDK listening on", baseJSURL) + wg.Done() + srv.Serve(ln) + fmt.Println("JS SDK closing webserver") + } + go startServer() + wg.Wait() + + // now login + createClientOpts := map[string]interface{}{ + "baseUrl": opts.BaseURL, + "useAuthorizationHeader": true, + "userId": opts.UserID, + } + if opts.DeviceID != "" { + createClientOpts["deviceId"] = opts.DeviceID + } + createClientOptsJSON, err := json.Marshal(createClientOpts) + if err != nil { + return nil, fmt.Errorf("failed to serialise login info: %s", err) + } + val := fmt.Sprintf("window.__client = matrix.createClient(%s);", string(createClientOptsJSON)) + fmt.Println(val) + // TODO: move to chrome package + var r *runtime.RemoteObject + err = chromedp.Run(ctx, + chromedp.Navigate(baseJSURL), + chromedp.Evaluate(val, &r), + ) + if err != nil { + return nil, fmt.Errorf("failed to go to %s and createClient: %s", baseJSURL, err) + } + // cannot use loginWithPassword as this generates a new device ID + chrome.AwaitExecute(t, ctx, fmt.Sprintf(`window.__client.login("m.login.password", { + user: "%s", + password: "%s", + device_id: "%s", + });`, opts.UserID, opts.Password, opts.DeviceID)) + chrome.AwaitExecute(t, ctx, `window.__client.initRustCrypto();`) + + // 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()+"||"+event.getEffectiveEvent().content.body); + });`, CONSOLE_LOG_CONTROL_STRING)) + + jsc.ctx = ctx + jsc.cancel = cancel + jsc.baseJSURL = baseJSURL + return jsc, nil +} + +// Close is called to clean up resources. +// Specifically, we need to shut off existing browsers and any FFI bindings. +// If we get callbacks/events after this point, tests may panic if the callbacks +// log messages. +func (c *JSClient) Close(t *testing.T) { + c.cancel() + c.listeners = make(map[int32]func(roomID string, text string)) } // 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()) { - return + chrome.AwaitExecute(t, c.ctx, `window.__client.startClient({});`) + return func() { + chrome.AwaitExecute(t, c.ctx, `window.__client.stopClient();`) + } } // 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(roomID string) (bool, error) { - return false, nil +func (c *JSClient) IsRoomEncrypted(t *testing.T, roomID string) (bool, error) { + isEncrypted, err := chrome.ExecuteInto[bool]( + t, c.ctx, fmt.Sprintf(`window.__client.isRoomEncrypted("%s")`, roomID), + ) + if err != nil { + return false, err + } + return *isEncrypted, nil } // SendMessage sends the given text as an m.room.message with msgtype:m.text into the given -// room. Returns the event ID of the sent event. +// room. func (c *JSClient) SendMessage(t *testing.T, roomID, text string) { - return + err := chrome.AwaitExecute(t, c.ctx, fmt.Sprintf(`window.__client.sendMessage("%s", { + "msgtype": "m.text", + "body": "%s" + });`, roomID, text)) + must.NotError(t, "failed to sendMessage", err) } func (c *JSClient) WaitUntilEventInRoom(t *testing.T, roomID, wantBody string) Waiter { - return nil + // TODO: check if this event already exists + return &jsTimelineWaiter{ + roomID: roomID, + wantBody: wantBody, + client: c, + } +} + +func (c *JSClient) Type() ClientType { + return ClientTypeJS +} + +func (c *JSClient) listenForUpdates(callback func(roomID, gotText string)) (cancel func()) { + id := c.listenerID.Add(1) + c.listeners[id] = callback + return func() { + delete(c.listeners, id) + } +} + +type jsTimelineWaiter struct { + roomID string + wantBody string + client *JSClient +} + +func (w *jsTimelineWaiter) Wait(t *testing.T, s time.Duration) { + updates := make(chan bool, 3) + cancel := w.client.listenForUpdates(func(roomID, gotText string) { + if w.roomID != roomID { + return + } + if w.wantBody != gotText { + return + } + updates <- true + }) + defer cancel() + + start := time.Now() + for { + timeLeft := s - time.Since(start) + if timeLeft <= 0 { + t.Fatalf("%s: Wait[%s]: timed out", w.client.userID, w.roomID) + } + select { + case <-time.After(timeLeft): + t.Fatalf("%s: Wait[%s]: timed out", w.client.userID, w.roomID) + case <-updates: + return + } + } +} + +func (w *jsTimelineWaiter) callback(gotRoomID, gotText string) { + if w.roomID == gotRoomID && w.wantBody == gotText { + + } } diff --git a/internal/api/rust.go b/internal/api/rust.go index f16b091..be95258 100644 --- a/internal/api/rust.go +++ b/internal/api/rust.go @@ -24,7 +24,7 @@ type RustClient struct { syncService *matrix_sdk_ffi.SyncService } -func NewRustClient(opts ClientCreationOpts, ssURL string) (Client, error) { +func NewRustClient(t *testing.T, opts ClientCreationOpts, ssURL string) (Client, error) { ab := matrix_sdk_ffi.NewClientBuilder().HomeserverUrl(opts.BaseURL).SlidingSyncProxy(&ssURL) client, err := ab.Build() if err != nil { @@ -46,11 +46,8 @@ func NewRustClient(opts ClientCreationOpts, ssURL string) (Client, error) { }, nil } -// Init is called prior to any test execution. Do any setup code here e.g run a browser. -// Call close() when the test terminates to clean up resources. -// TODO: will this be too slow if we spin up a browser for each test? -func (c *RustClient) Init(t *testing.T) (close func()) { - return func() {} +func (c *RustClient) Close(t *testing.T) { + c.FFIClient.Destroy() } // StartSyncing to begin syncing from sync v2 / sliding sync. @@ -69,7 +66,7 @@ func (c *RustClient) 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 *RustClient) IsRoomEncrypted(roomID string) (bool, error) { +func (c *RustClient) IsRoomEncrypted(t *testing.T, roomID string) (bool, error) { r := c.findRoom(roomID) if r == nil { rooms := c.FFIClient.Rooms() @@ -87,10 +84,15 @@ func (c *RustClient) WaitUntilEventInRoom(t *testing.T, roomID, wantBody string) } } +func (c *RustClient) Type() ClientType { + return ClientTypeRust +} + // SendMessage sends the given text as an m.room.message with msgtype:m.text into the given // room. Returns the event ID of the sent event. func (c *RustClient) SendMessage(t *testing.T, roomID, text string) { - // we need a timeline listener before we can send messages + // we need a timeline listener before we can send messages, AND that listener must be attached to the + // same *Room you call .Send on :S r := c.ensureListening(t, roomID) t.Logf("%s: SendMessage[%s]: '%s'", c.userID, roomID, text) r.Send(matrix_sdk_ffi.MessageEventContentFromHtml(text, text)) diff --git a/internal/chrome/exec.go b/internal/chrome/exec.go index 7f52db4..132ad95 100644 --- a/internal/chrome/exec.go +++ b/internal/chrome/exec.go @@ -44,3 +44,13 @@ func MustAwaitExecute(t *testing.T, ctx context.Context, js string) { err := AwaitExecute(t, ctx, js) must.NotError(t, js, err) } + +func MustExecute(t *testing.T, ctx context.Context, js string) { + t.Helper() + var r *runtime.RemoteObject // stop large responses causing errors "Object reference chain is too long (-32000)" + t.Log(js) + err := chromedp.Run(ctx, + chromedp.Evaluate(js, &r), + ) + must.NotError(t, js, err) +} diff --git a/rebuild_js_sdk.sh b/rebuild_js_sdk.sh index cada3dd..2b33f8c 100755 --- a/rebuild_js_sdk.sh +++ b/rebuild_js_sdk.sh @@ -16,5 +16,5 @@ then fi (cd js-sdk && yarn add $1 && yarn install && yarn build) -rm -rf ./tests/dist || echo 'no dist directory detected'; -cp -r ./js-sdk/dist/. ./tests/dist \ No newline at end of file +rm -rf ./internal/api/dist || echo 'no dist directory detected'; +cp -r ./js-sdk/dist/. ./internal/api/dist diff --git a/tests/happy_test.go b/tests/happy_test.go new file mode 100644 index 0000000..a1e8364 --- /dev/null +++ b/tests/happy_test.go @@ -0,0 +1,103 @@ +package tests + +import ( + "testing" + "time" + + "github.com/matrix-org/complement-crypto/internal/api" + "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/complement/must" +) + +// The simplest test case. +// Alice creates the room. Bob joins. +// Alice sends an encrypted message. +// Ensure Bob can see the decrypted content. +// +// Caveats: because this exercises the high level API, we do not explicitly +// say "send an encrypted event". The only indication that encrypted events are +// being sent is the m.room.encryption state event on /createRoom, coupled with +// asserting that isEncrypted() returns true. This test may be expanded in the +// future to assert things like "there is a ciphertext". +func TestAliceBobEncryptionWorks(t *testing.T) { + // TODO: factor out so we can just call "matrix subtests" + t.Run("Rust x Rust", func(t *testing.T) { + testAliceBobEncryptionWorks(t, api.ClientTypeRust, api.ClientTypeRust) + }) + t.Run("JS x JS", func(t *testing.T) { + testAliceBobEncryptionWorks(t, api.ClientTypeJS, api.ClientTypeJS) + }) + t.Run("Rust x JS", func(t *testing.T) { + testAliceBobEncryptionWorks(t, api.ClientTypeRust, api.ClientTypeJS) + }) + t.Run("JS x Rust", func(t *testing.T) { + testAliceBobEncryptionWorks(t, api.ClientTypeJS, api.ClientTypeRust) + }) +} + +func testAliceBobEncryptionWorks(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: "testfromrustsdk", + }) + csapiBob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ + LocalpartSuffix: "bob", + Password: "testfromrustsdk", + }) + roomID := csapiAlice.MustCreateRoom(t, map[string]interface{}{ + "name": "JS SDK Test", + "preset": "trusted_private_chat", + "invite": []string{csapiBob.UserID}, + "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 + // ----------------- + alice := MustLoginClient(t, clientTypeA, api.FromComplementClient(csapiAlice, "testfromrustsdk"), ss) + defer alice.Close(t) + + // Alice starts syncing + aliceStopSyncing := alice.StartSyncing(t) + defer aliceStopSyncing() + time.Sleep(time.Second) // TODO: find another way to wait until initial sync is done + + wantMsgBody := "Hello world" + + // Check the room is in fact encrypted + isEncrypted, err := alice.IsRoomEncrypted(t, roomID) + must.NotError(t, "failed to check if room is encrypted", err) + must.Equal(t, isEncrypted, true, "room is not encrypted when it should be") + + // Bob starts syncing + bob := MustLoginClient(t, clientTypeB, api.FromComplementClient(csapiBob, "testfromrustsdk"), ss) + defer bob.Close(t) + bobStopSyncing := bob.StartSyncing(t) + defer bobStopSyncing() + time.Sleep(time.Second) // TODO: find another way to wait until initial sync is done + + isEncrypted, err = bob.IsRoomEncrypted(t, roomID) + must.NotError(t, "failed to check if room is encrypted", err) + must.Equal(t, isEncrypted, true, "room is not encrypted") + t.Logf("bob room encrypted = %v", isEncrypted) + + waiter := bob.WaitUntilEventInRoom(t, roomID, wantMsgBody) + alice.SendMessage(t, roomID, wantMsgBody) + + // Bob receives the message + t.Logf("bob (%s) waiting for event", bob.Type()) + waiter.Wait(t, 5*time.Second) +} diff --git a/tests/js_test.go b/tests/js_test.go deleted file mode 100644 index ff267c1..0000000 --- a/tests/js_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package tests - -import ( - "context" - "embed" - "encoding/json" - "fmt" - "io/fs" - "log" - "net" - "net/http" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/chromedp/cdproto/runtime" - "github.com/chromedp/chromedp" - "github.com/matrix-org/complement-crypto/internal/chrome" - "github.com/matrix-org/complement/client" - "github.com/matrix-org/complement/helpers" - "github.com/matrix-org/complement/must" -) - -//go:embed dist -var jsSDKDistDirectory embed.FS - -func setupClient(t *testing.T, ctx context.Context, jsSDKURL string, csapi *client.CSAPI) { - // bob syncs and joins the room - createClientOpts := map[string]interface{}{ - "baseUrl": csapi.BaseURL, - "useAuthorizationHeader": true, - "userId": csapi.UserID, - "deviceId": csapi.DeviceID, - "accessToken": csapi.AccessToken, - } - createClientOptsJSON, err := json.Marshal(createClientOpts) - must.NotError(t, "failed to serialise json", err) - val := fmt.Sprintf("window.__client = matrix.createClient(%s);", string(createClientOptsJSON)) - fmt.Println(val) - var r *runtime.RemoteObject - err = chromedp.Run(ctx, - chromedp.Navigate(jsSDKURL), - chromedp.Evaluate(val, &r), - ) - must.NotError(t, "failed to go to new page", err) - chrome.AwaitExecute(t, ctx, `window.__client.initRustCrypto();`) - chrome.AwaitExecute(t, ctx, `window.__client.startClient({});`) - time.Sleep(2 * time.Second) -} - -func TestJS(t *testing.T) { - deployment := Deploy(t) - // pre-register alice and bob - csapiAlice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "alice", - Password: "testfromjsdk", - }) - csapiBob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "bob", - Password: "testfromrustsdk", - }) - roomID := csapiAlice.MustCreateRoom(t, map[string]interface{}{ - "name": "JS SDK Test", - "preset": "trusted_private_chat", - "invite": []string{csapiBob.UserID}, - "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"}) - - // start a headless chrome - ctx, cancel := chromedp.NewContext(context.Background(), chromedp.WithBrowserOption( - chromedp.WithBrowserLogf(log.Printf), chromedp.WithBrowserErrorf(log.Printf), //chromedp.WithBrowserDebugf(log.Printf), - )) - // log console.log and listen for callbacks - preparedWaiter := helpers.NewWaiter() - chromedp.ListenTarget(ctx, func(ev interface{}) { - switch ev := ev.(type) { - case *runtime.EventConsoleAPICalled: - for _, arg := range ev.Args { - s, err := strconv.Unquote(string(arg.Value)) - if err != nil { - log.Println(err) - continue - } - fmt.Printf("%s\n", s) - if strings.HasPrefix(s, "JS_SDK_TEST:") { - val := strings.TrimPrefix(s, "JS_SDK_TEST:") - fmt.Println(">>>>>>> ", val) - if val == "SYNCING" { - preparedWaiter.Finish() - } - } - } - } - }) - defer cancel() - - // run js-sdk (need to run this as a web server to avoid CORS errors you'd otherwise get with file: URLs) - var wg sync.WaitGroup - wg.Add(2) - baseJSURL := "" - baseJSURL2 := "" - c, err := fs.Sub(jsSDKDistDirectory, "dist") - if err != nil { - panic(err) - } - http.Handle("/", http.FileServer(http.FS(c))) - startServer := func(fn func(u string)) { - srv := &http.Server{ - Addr: "127.0.0.1:0", - Handler: http.DefaultServeMux, - } - ln, err := net.Listen("tcp", srv.Addr) - if err != nil { - panic(err) - } - - u := "http://" + ln.Addr().String() - fmt.Println("listening on", u) - fn(u) - wg.Done() - srv.Serve(ln) - fmt.Println("closing webserver") - } - go startServer(func(u string) { - baseJSURL = u - }) - go startServer(func(u string) { - baseJSURL2 = u - }) - wg.Wait() - - setupClient(t, ctx, baseJSURL2, csapiBob) - - fmt.Println("hs", csapiAlice.BaseURL) - - // run task list - createClientOpts := map[string]interface{}{ - "baseUrl": csapiAlice.BaseURL, - "useAuthorizationHeader": true, - "userId": csapiAlice.UserID, - "deviceId": csapiAlice.DeviceID, - "accessToken": csapiAlice.AccessToken, - } - createClientOptsJSON, err := json.Marshal(createClientOpts) - must.NotError(t, "failed to serialise json", err) - val := fmt.Sprintf("window._test_client = matrix.createClient(%s);", string(createClientOptsJSON)) - fmt.Println(val) - //time.Sleep(time.Hour) - var r *runtime.RemoteObject - err = chromedp.Run(ctx, - chromedp.Navigate(baseJSURL), - chromedp.Evaluate(val, &r), - ) - must.NotError(t, "failed to createClient", err) - chrome.AwaitExecute(t, ctx, `window._test_client.initRustCrypto();`) - - // add listener for the room to appear - err = chromedp.Run(ctx, - chromedp.Evaluate(`window._test_client.on("sync", function(state){ - console.log("JS_SDK_TEST:" + state); - });`, &r), - ) - must.NotError(t, "failed to listen for prepared callback", err) - - // start syncing - chrome.AwaitExecute(t, ctx, `window._test_client.startClient({});`) - - // wait for the room to appear - preparedWaiter.Wait(t, 5*time.Second) - - // check room is encrypted - must.Equal(t, chrome.MustExecuteInto[bool]( - t, ctx, fmt.Sprintf(`window._test_client.isRoomEncrypted("%s")`, roomID), - ), true, "room is not encrypted") - - // send an encrypted message - chrome.AwaitExecute(t, ctx, fmt.Sprintf(`window._test_client.sendMessage("%s", { - "msgtype": "m.text", - "body": "Hello World!" - });`, roomID)) - must.NotError(t, "failed to send message", err) - - time.Sleep(2 * time.Second) // wait for keys/changes - - // bob syncs and joins the room - createClientOpts = map[string]interface{}{ - "baseUrl": csapiBob.BaseURL, - "useAuthorizationHeader": true, - "userId": csapiBob.UserID, - "deviceId": csapiBob.DeviceID, - "accessToken": csapiBob.AccessToken, - } - createClientOptsJSON, err = json.Marshal(createClientOpts) - must.NotError(t, "failed to serialise json", err) - val = fmt.Sprintf("window._test_bob = matrix.createClient(%s);", string(createClientOptsJSON)) - fmt.Println(val) - err = chromedp.Run(ctx, - chromedp.Navigate(baseJSURL2), - chromedp.Evaluate(val, &r), - ) - must.NotError(t, "failed to go to new page", err) - chrome.AwaitExecute(t, ctx, `window._test_bob.initRustCrypto();`) - chrome.AwaitExecute(t, ctx, `window._test_bob.startClient({});`) - - // ensure bob sees the decrypted event - time.Sleep(time.Second) - - execute(t, ctx, `window._bob_room = window._test_bob.getRoom("%s")`, roomID) - execute(t, ctx, `window._bob_tl = window._bob_room.getLiveTimeline().getEvents()`) - //var tl map[string]interface{} - //executeInto(t, ctx, &tl, `window._bob_tl[window._bob_tl.length-1].event`) - tl := chrome.MustExecuteInto[map[string]interface{}](t, ctx, `window._bob_tl[window._bob_tl.length-1].event`) - t.Logf("%+v", tl) - tl = chrome.MustExecuteInto[map[string]interface{}](t, ctx, `window._bob_tl[window._bob_tl.length-1].getEffectiveEvent()`) - t.Logf("%+v", tl) - isEncrypted := chrome.MustExecuteInto[bool](t, ctx, `window._bob_tl[window._bob_tl.length-1].isEncrypted()`) - t.Logf("%v", isEncrypted) - must.Equal(t, isEncrypted, true, "room is not encrypted") -} - -func execute(t *testing.T, ctx context.Context, cmd string, args ...interface{}) { - t.Helper() - var r *runtime.RemoteObject // stop large responses causing errors "Object reference chain is too long (-32000)" - js := fmt.Sprintf(cmd, args...) - t.Log(js) - err := chromedp.Run(ctx, - chromedp.Evaluate(js, &r), - ) - must.NotError(t, js, err) -} - -func executeInto(t *testing.T, ctx context.Context, res interface{}, cmd string, args ...interface{}) { - t.Helper() - js := fmt.Sprintf(cmd, args...) - t.Log(js) - err := chromedp.Run(ctx, - chromedp.Evaluate(js, &res), - ) - must.NotError(t, js, err) -} diff --git a/tests/main_test.go b/tests/main_test.go index e9acb9e..5362682 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -5,7 +5,9 @@ import ( "testing" "github.com/matrix-org/complement" + "github.com/matrix-org/complement-crypto/internal/api" "github.com/matrix-org/complement-crypto/internal/deploy" + "github.com/matrix-org/complement/must" ) var ( @@ -35,3 +37,19 @@ func Deploy(t *testing.T) *deploy.SlidingSyncDeployment { ssDeployment = deploy.RunNewDeployment(t) return ssDeployment } + +func MustLoginClient(t *testing.T, clientType api.ClientType, opts api.ClientCreationOpts, ssURL string) api.Client { + switch clientType { + case api.ClientTypeRust: + c, err := api.NewRustClient(t, opts, ssURL) + must.NotError(t, "NewRustClient: %s", err) + return c + case api.ClientTypeJS: + c, err := api.NewJSClient(t, opts) + must.NotError(t, "NewJSClient: %s", err) + return c + default: + t.Fatalf("unknown client type %v", clientType) + } + panic("unreachable") +} diff --git a/tests/rust_test.go b/tests/rust_test.go deleted file mode 100644 index 74998ff..0000000 --- a/tests/rust_test.go +++ /dev/null @@ -1,222 +0,0 @@ -package tests - -import ( - "testing" - "time" - - "github.com/matrix-org/complement-crypto/internal/api" - "github.com/matrix-org/complement-crypto/rust/matrix_sdk_ffi" - - "github.com/matrix-org/complement/helpers" - "github.com/matrix-org/complement/must" -) - -func TestCreateRoom(t *testing.T) { - deployment := Deploy(t) - // pre-register alice and bob - csapiAlice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "alice", - Password: "testfromrustsdk", - }) - csapiBob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "bob", - Password: "testfromrustsdk", - }) - ss := deployment.SlidingSyncURL(t) - - // Rust SDK testing below - // ---------------------- - - // Alice creates an encrypted room - ab := matrix_sdk_ffi.NewClientBuilder().HomeserverUrl(csapiAlice.BaseURL).SlidingSyncProxy(&ss) - alice, err := ab.Build() - must.NotError(t, "client builder failed to build", err) - must.NotError(t, "failed to login", alice.Login(csapiAlice.UserID, "testfromrustsdk", nil, nil)) - roomName := "Rust SDK Test" - roomID, err := alice.CreateRoom(matrix_sdk_ffi.CreateRoomParameters{ - Name: &roomName, - Visibility: matrix_sdk_ffi.RoomVisibilityPublic, - Preset: matrix_sdk_ffi.RoomPresetPublicChat, - IsEncrypted: true, - }) - must.NotError(t, "failed to create room", err) - must.NotEqual(t, roomID, "", "empty room id") - t.Logf("created room %s", roomID) - wantMsgBody := "Hello world" - - // Alice starts syncing - aliceSync, err := alice.SyncService().FinishBlocking() - must.NotError(t, "failed to make sync service", err) - go aliceSync.StartBlocking() - defer aliceSync.StopBlocking() - time.Sleep(time.Second) - - // Alice gets the room she created - t.Logf("alice getting rooms") - aliceRooms := alice.Rooms() - must.Equal(t, len(aliceRooms), 1, "room missing from Rooms()") - aliceRoom := aliceRooms[0] - must.Equal(t, aliceRoom.Id(), roomID, "mismatched room IDs") - enc, err := aliceRoom.IsEncrypted() - must.NotError(t, "failed to check if room is encrypted", err) - must.Equal(t, enc, true, "room is not encrypted when it should be") - // we need a timeline listener before we can send messages - aliceRoom.AddTimelineListenerBlocking(&timelineListener{fn: func(diff []*matrix_sdk_ffi.TimelineDiff) { - - }}) - - // Alice invites Bob - must.NotError(t, "failed to invite bob", aliceRoom.InviteUserById(csapiBob.UserID)) - - // Bob starts syncing - bb := matrix_sdk_ffi.NewClientBuilder().HomeserverUrl(csapiBob.BaseURL).SlidingSyncProxy(&ss) - bob, err := bb.Build() - must.NotError(t, "client builder failed to build", err) - must.NotError(t, "failed to login", bob.Login(csapiBob.UserID, "testfromrustsdk", nil, nil)) - bobSync, err := bob.SyncService().FinishBlocking() - must.NotError(t, "failed to make sync service", err) - go bobSync.StartBlocking() - defer bobSync.StopBlocking() - time.Sleep(time.Second) - - // Bob gets the room he was invited to - t.Logf("bob getting rooms") - bobRooms := bob.Rooms() - must.Equal(t, len(bobRooms), 1, "room missing from Rooms()") - bobRoom := bobRooms[0] - must.Equal(t, bobRoom.Id(), roomID, "mismatched room IDs") - // we need a timeline listener before we can send messages - var bobMsgs []string - waiter := helpers.NewWaiter() - bobRoom.AddTimelineListenerBlocking(&timelineListener{fn: func(diff []*matrix_sdk_ffi.TimelineDiff) { - var items []*matrix_sdk_ffi.TimelineItem - for _, d := range diff { - t.Logf("diff %v", d.Change()) - switch d.Change() { - case matrix_sdk_ffi.TimelineChangeInsert: - insertData := d.Insert() - if insertData == nil { - continue - } - items = append(items, insertData.Item) - case matrix_sdk_ffi.TimelineChangeAppend: - appendItems := d.Append() - if appendItems == nil { - continue - } - items = append(items, *appendItems...) - case matrix_sdk_ffi.TimelineChangePushBack: - pbData := d.PushBack() - if pbData == nil { - continue - } - items = append(items, *pbData) - case matrix_sdk_ffi.TimelineChangeSet: - setData := d.Set() - if setData == nil { - continue - } - items = append(items, setData.Item) - } - } - for _, item := range items { - t.Logf("handle item %v", item.FmtDebug()) - ev := item.AsEvent() - if ev == nil { - continue - } - evv := *ev - msg := evv.Content().AsMessage() - if msg == nil { - continue - } - msgg := *msg - bobMsgs = append(bobMsgs, msgg.Body()) - t.Logf("bob got item: %s", msgg.Body()) - if msgg.Body() == wantMsgBody { - waiter.Finish() - } - } - }}) - - // Bob accepts the invite - must.NotError(t, "bob failed to join room", bobRoom.Join()) - - // Alice sends a message - aliceRoom.Send(matrix_sdk_ffi.MessageEventContentFromHtml(wantMsgBody, wantMsgBody)) - - // Bob receives the message - waiter.Wait(t, time.Second) -} - -func TestCreateRoomGeneric(t *testing.T) { - deployment := Deploy(t) - // pre-register alice and bob - csapiAlice := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "alice", - Password: "testfromrustsdk", - }) - csapiBob := deployment.Register(t, "hs1", helpers.RegistrationOpts{ - LocalpartSuffix: "bob", - Password: "testfromrustsdk", - }) - roomID := csapiAlice.MustCreateRoom(t, map[string]interface{}{ - "name": "JS SDK Test", - "preset": "trusted_private_chat", - "invite": []string{csapiBob.UserID}, - "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) - - // Rust SDK testing below - // ---------------------- - alice, err := api.NewRustClient(api.FromComplementClient(csapiAlice, "testfromrustsdk"), ss) - must.NotError(t, "failed to make new rust client", err) - - // Alice starts syncing - aliceStopSyncing := alice.StartSyncing(t) - defer aliceStopSyncing() - time.Sleep(time.Second) // TODO: find another way to wait until initial sync is done - - wantMsgBody := "Hello world" - - // Check the room is in fact encrypted - isEncrypted, err := alice.IsRoomEncrypted(roomID) - must.NotError(t, "failed to check if room is encrypted", err) - must.Equal(t, isEncrypted, true, "room is not encrypted when it should be") - - // Bob starts syncing - bob, err := api.NewRustClient(api.FromComplementClient(csapiBob, "testfromrustsdk"), ss) - must.NotError(t, "failed to make new rust client", err) - bobStopSyncing := bob.StartSyncing(t) - defer bobStopSyncing() - time.Sleep(time.Second) // TODO: find another way to wait until initial sync is done - - isEncrypted, err = bob.IsRoomEncrypted(roomID) - must.NotError(t, "failed to check if room is encrypted", err) - must.Equal(t, isEncrypted, true, "room is not encrypted") - t.Logf("bob room encrypted = %v", isEncrypted) - - waiter := bob.WaitUntilEventInRoom(t, roomID, wantMsgBody) - alice.SendMessage(t, roomID, wantMsgBody) - - // Bob receives the message - waiter.Wait(t, 5*time.Second) -} - -type timelineListener struct { - fn func(diff []*matrix_sdk_ffi.TimelineDiff) -} - -func (l *timelineListener) OnUpdate(diff []*matrix_sdk_ffi.TimelineDiff) { - l.fn(diff) -}