Compare commits

...

417 Commits

Author SHA1 Message Date
RiotRobot 1842004db2 v21.0.0 2022-10-25 17:04:02 +01:00
RiotRobot c8c7af0ae2 Prepare changelog for v21.0.0 2022-10-25 17:04:02 +01:00
RiotRobot e7ce1fb9e8 v21.0.0-rc.2 2022-10-24 16:19:47 +01:00
RiotRobot 7772f855e6 Prepare changelog for v21.0.0-rc.2 2022-10-24 16:19:46 +01:00
ElementRobot 4ccc52da8e [Backport staging] Improve crypto init code and allow easier shimming (#2792)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-24 10:12:42 +01:00
ElementRobot 63f4bf571e [Backport staging] Fix POST data not being passed for registerWithIdentityServer (#2770)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-18 15:03:40 +00:00
RiotRobot fc1b03c0bf v21.0.0-rc.1 2022-10-18 12:49:22 +01:00
RiotRobot e592f60240 Prepare changelog for v21.0.0-rc.1 2022-10-18 12:49:21 +01:00
Michael Telatynski 30570bcce6 Remove node-specific crypto bits, use Node 16's WebCrypto (#2762) 2022-10-17 17:54:54 +01:00
Michael Telatynski 6af3b114e1 Fix IdentityPrefix.V2 containing spurious /api (#2761) 2022-10-17 14:32:40 +01:00
Michael Telatynski 041f9951c5 Remove deprecated m.room.aliases references (#2759) 2022-10-17 10:58:48 +01:00
Germain be11fa6b5a Fix notification type when resetting threads notifications (#2757) 2022-10-17 09:27:28 +01:00
Stanislav Demydiuk 6245661cd7 Export types for MatrixEvent and Room emitted events, and make event handler map types stricter (#2750) 2022-10-14 21:18:00 -04:00
Michael Telatynski 12a4d2a749 Make more of the code conform to Strict TSC (#2756) 2022-10-14 15:57:08 +01:00
kegsay f70f6db926 Merge pull request #2753 from matrix-org/kegan/http-code-on-non-json
Always send back an httpStatus property if one is known
2022-10-14 10:31:12 +01:00
Kegan Dougal 500601ea85 More tests to satisfy sonarcloud 2022-10-14 10:23:18 +01:00
Kegan Dougal e32dfccbd9 Additional tests for 'url' property to satisfy code coverage 2022-10-13 15:39:23 +01:00
Kegan Dougal ed78737768 Fix types on utils UTs 2022-10-13 15:19:26 +01:00
Kegan Dougal ac561b743b Linting 2022-10-13 15:04:01 +01:00
Kegan Dougal e8be7af751 err already sets httpStatus now 2022-10-13 15:02:52 +01:00
Kegan Dougal 400b457edf Fix broken tests 2022-10-13 14:59:15 +01:00
Kegan Dougal 5ed4e9f535 Always send back an httpStatus property if one is known
Previously, non-JSON responses would be missing the `httpStatus`
property, which was different to how `request()` used to work.

Ensure we always send this property, even for non-JSON responses.
2022-10-13 14:53:03 +01:00
Germain c81d759334 Emit events when setting unread notifications (#2748)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-10-13 14:09:33 +01:00
kegsay 50dd79c595 Check for AbortError, not any generic connection error, to avoid tightlooping (#2752) 2022-10-13 12:18:31 +00:00
kegsay 2eb0afbad5 Merge pull request #2605 from matrix-org/kegan/sync-v3
Automatically reconnect sessions when sliding sync expires them
2022-10-13 12:53:32 +01:00
Kegan Dougal 007ca97741 Linting 2022-10-13 11:21:27 +01:00
Kegan Dougal f81e53c908 Rejig timeout function to cancel the timer when listenUntil returns 2022-10-13 11:17:44 +01:00
Kegan Dougal 89d743ac48 New style abort 2022-10-13 11:07:34 +01:00
kegsay fd61a49157 Merge branch 'develop' into kegan/sync-v3 2022-10-13 10:56:36 +01:00
Kegan Dougal bb3d51652d Consolidate error handling retry logic 2022-10-13 10:52:40 +01:00
Michael Telatynski bbece73346 Improve MatrixError message (#2749) 2022-10-13 09:07:15 +01:00
Travis Ralston cc025ea458 Fix more key backup paths being unstable (#2746) 2022-10-12 18:10:47 +00:00
renovate[bot] d06a3a47c3 Lock file maintenance (#2743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-12 18:07:58 +00:00
Michael Telatynski 34c5598a3f Modernize http-api - move from browser-request to fetch (#2719) 2022-10-12 18:59:04 +01:00
Kegan Dougal 41a973a3c6 Merge branch 'develop' into kegan/sync-v3 2022-10-12 17:22:52 +01:00
Dominik Henneke 913660c818 Correct the dir parameter of MSC3715 (#2745) 2022-10-12 16:56:50 +02:00
RiotRobot 8eed354e17 Resetting package fields for development 2022-10-11 13:43:31 +01:00
RiotRobot 7e12b62b7c Merge branch 'master' into develop 2022-10-11 13:43:28 +01:00
RiotRobot aa5a34948a v20.1.0 2022-10-11 13:41:59 +01:00
RiotRobot 6ba35e9fbc Prepare changelog for v20.1.0 2022-10-11 13:41:58 +01:00
Michael Telatynski fe2c35092e Upgrade to Olm 3.2.13 which has been repackaged to support Node 18 (#2744) 2022-10-10 13:34:33 +01:00
Šimon Brandner e37aab2967 Fix power_level_content_override type (#2741) 2022-10-07 16:40:40 +02:00
Germain 62007ec673 Fix sync init when thread unread notif is not supported (#2739)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-07 10:38:53 +00:00
Travis Ralston 029280b9d9 Update hidden characters regex (#2738) 2022-10-06 22:41:57 -06:00
Šimon Brandner 6e5326f9c8 Add custom notification handling for MSC3401 call events (#2720) 2022-10-06 16:40:30 +02:00
Kerry a1b046b5d8 test typescriptification - spec/integ (#2714)
* renamed:    spec/integ/devicelist-integ.spec.js -> spec/integ/devicelist-integ.spec.ts

* fix ts issue in devicelist-integ.spec

* renamed:    spec/integ/matrix-client-event-emitter.spec.js -> spec/integ/matrix-client-event-emitter.spec.ts

* ts issues in matrix-client-event-emitter integ

* strict fixes

* renamed:    spec/integ/matrix-client-methods.spec.js -> spec/integ/matrix-client-methods.spec.ts

* fix ts issues

* renamed:    spec/integ/matrix-client-opts.spec.js -> spec/integ/matrix-client-opts.spec.ts

* ts fixes in matrix-client-methods / matrix-client-opts

* renamed:    spec/integ/matrix-client-room-timeline.spec.js -> spec/integ/matrix-client-room-timeline.spec.ts

* most ts fixes in matrix-client-room-timeline

* remove obsoleted prev_events from mockenvents

* make xmlhttprequest ts

* strict errors in matrix-client-event-timeline spec

* strict in devicelist

* strict fixes in matrix-client-crypto.spec

* strict fixes in spec/integ/matrix-client-room-timeline

* strict issues in matrix-client-opts.specc

* strict issues in matrix-client-syncing

* strict issues in spec/integ/megolm

* strict fixes in spec/integ/matrix-client-retrying.spec

* strict fixes for spec/integ/sliding-sync

* eslint fixes

* more strict errors sneaking in from develop

* kill al httpbackends

* kill matrix-client-methods.spec httpbackend properly
2022-10-06 08:11:25 +02:00
Janne Mareike Koschinski 3a3dcfb254 Load Thread List with server-side assistance (MSC3856) (#2602)
* feature detection code for thread list api
* fix bug where createThreadsTimelineSets would sometimes return nothing
* initial implementation of thread listing msc
* tests for thread list pagination
2022-10-05 23:10:42 +02:00
RiotRobot 121250a6fb v20.1.0-rc.2 2022-10-05 13:23:42 +01:00
RiotRobot a7aa227f55 Prepare changelog for v20.1.0-rc.2 2022-10-05 13:23:42 +01:00
Germain 21a6f61b7b Add support for unread thread notifications (#2726) 2022-10-05 10:37:45 +01:00
renovate[bot] ff720e3aa3 Update all (#2732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-04 22:43:50 -06:00
renovate[bot] 2935daeb3f Update typescript-eslint monorepo to v5.39.0 (#2733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-04 22:43:38 -06:00
ElementRobot 04c1dfe43a Use the correct sender key when checking shared secret (#2730) (#2731)
(cherry picked from commit 890a840685)

Co-authored-by: Hubert Chathi <hubertc@matrix.org>
2022-10-04 11:27:40 -06:00
Hubert Chathi 890a840685 Use the correct sender key when checking shared secret (#2730) 2022-10-04 13:00:45 -04:00
Travis Ralston b1ed972867 Use stable calls to /room_keys (#2729)
* Use stable calls to `/room_keys`

Fixes https://github.com/vector-im/element-web/issues/22839

* Appease the CI
2022-10-04 10:55:16 -06:00
Kerry 2f24e90e53 Device manager - last_seen_user_agent device property (#2728)
* add last_seen_user_agent to IMyDevice type

* add ubstable value

* use unstable value in interface
2022-10-04 15:50:14 +00:00
RiotRobot 07476a0ae0 v20.1.0-rc.1 2022-10-04 14:01:35 +01:00
RiotRobot 6348704bec Prepare changelog for v20.1.0-rc.1 2022-10-04 14:01:34 +01:00
mcalinghee f84a33910c Unexpected ignored self key request when it's not shared history (#2724)
* ignore forwarded key process also if the user is not the same
2022-10-04 14:31:21 +02:00
Germain 0ccf7c50f2 Add custom error message for restricted import (#2727) 2022-10-04 09:59:20 +01:00
Šimon Brandner ead33003b7 Check for the spec version when determining private read receipt support (#2722) 2022-10-04 06:50:42 +00:00
Faye Duxovni 7d5360a00f Rename redecryption-related function arguments for clarity (#2709) 2022-10-03 19:51:30 -04:00
renovate[bot] 4b283015ba Lock file maintenance (#2721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-03 08:30:28 +01:00
Michael Telatynski 887e15aac5 Fix IDB initial migration handling causing spurious lazy loading upgrade loops (#2718) 2022-09-30 10:41:04 +00:00
RiotRobot a83d80f502 Merge branch 'master' into develop 2022-09-30 10:59:52 +01:00
RiotRobot 6166a8f7fd v20.0.2 2022-09-30 10:58:34 +01:00
RiotRobot 3efc18cfde Prepare changelog for v20.0.2 2022-09-30 10:58:33 +01:00
ElementRobot 5520aa3e2a [Backport staging] Fix issue in sync when crypto is not supported by client (#2716)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Stanislav Demydiuk <stas-demydiuk@users.noreply.github.com>
2022-09-30 10:57:04 +01:00
Michael Telatynski 5afe373446 Fix release-npm.yml dist-tag npm token passing mechanism (#2717) 2022-09-30 10:56:36 +01:00
Stanislav Demydiuk 9bb5afe5c0 Fix issue in sync when crypto is not supported by client (#2715)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-09-30 08:12:31 +00:00
Michael Telatynski f349663329 Add CI to protect against mixing src and lib imports (#2704) 2022-09-30 09:05:28 +01:00
Faye Duxovni f398e3564d Calculate IndexedDB versions automatically to reduce repeated information and possibilities for error (#2713) 2022-09-29 16:25:19 -04:00
RiotRobot 91171afddd Merge branch 'master' into develop 2022-09-28 17:48:45 +01:00
RiotRobot b54c9d689a v20.0.1 2022-09-28 17:47:24 +01:00
RiotRobot 4e69d7c9ac Prepare changelog for v20.0.1 2022-09-28 17:47:23 +01:00
ElementRobot bf9f595984 [Backport staging] Fix missing return when receiving an invitation without shared history (#2711)
Co-authored-by: Faye Duxovni <fayed@element.io>
2022-09-28 16:40:58 +00:00
Faye Duxovni f410e71bfa Fix missing return when receiving an invitation without shared history (#2710) 2022-09-28 17:36:09 +01:00
RiotRobot 83fca5b57d Merge branch 'master' into develop 2022-09-28 15:46:11 +01:00
RiotRobot 90052670e7 v20.0.0 2022-09-28 15:44:47 +01:00
RiotRobot db49a1a623 Prepare changelog for v20.0.0 2022-09-28 15:44:47 +01:00
ElementRobot 45330c6418 [Backport staging] Bump IDB crypto store version (#2708)
Co-authored-by: Faye Duxovni <fayed@element.io>
2022-09-28 15:43:43 +01:00
Faye Duxovni 4ba083e6af Bump IDB crypto store version (#2705)
* Bump IDB crypto store version

* lint fix
2022-09-28 15:39:37 +01:00
Germain 0403e4bedc Fix incorrect MSC3890 unstable prefix (#2703) 2022-09-28 15:37:13 +02:00
RiotRobot 14aa7846a5 Merge branch 'master' into develop 2022-09-28 14:05:21 +01:00
RiotRobot 2d067ad957 v19.7.0 2022-09-28 14:03:53 +01:00
RiotRobot 418aa3ff6a Prepare changelog for v19.7.0 2022-09-28 14:03:52 +01:00
RiotRobot a587d7c360 Resolve multiple CVEs
CVE-2022-39249
CVE-2022-39250
CVE-2022-39251
CVE-2022-39236
2022-09-28 13:55:15 +01:00
RiotRobot 45348a354e Resetting package fields for development 2022-09-27 16:48:03 +01:00
RiotRobot fa3339fc84 Merge branch 'master' into develop 2022-09-27 16:48:00 +01:00
RiotRobot b64a30f0ad v19.6.0 2022-09-27 16:46:53 +01:00
RiotRobot 5451f6139a Prepare changelog for v19.6.0 2022-09-27 16:46:52 +01:00
Germain b332c6c4b9 Account data can return undefined (#2701) 2022-09-27 12:47:05 +00:00
Germain 209a101be7 Add local notification settings capability (#2700) 2022-09-27 11:41:20 +01:00
Michael Telatynski efbf5479d1 Remove redundant rel_branch logic (#2698) 2022-09-26 10:05:58 +01:00
Hugh Nimmo-Smith dacef048be Typings for MSC3824 (#2691) 2022-09-23 16:23:50 -06:00
Hugh Nimmo-Smith caadc6f95b Implementation of MSC3882 login token request (#2687) 2022-09-22 16:36:37 +01:00
Hugh Nimmo-Smith 39bc7e2bb3 Refer to explicit Matrix spec versions instead of Latest (#2668) 2022-09-22 15:28:11 +00:00
Hugh Nimmo-Smith 040f012350 Typings for MSC2965 OIDC provider discovery (#2424) 2022-09-22 16:27:13 +01:00
Hugh Nimmo-Smith 8b2b677f92 Add return type for loginFlows() (#2669)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-09-22 16:25:01 +01:00
Germain 2b1fab928b Add unstable device_id field for MSC3881 (#2688) 2022-09-22 15:44:54 +02:00
Germain 516f52c5a4 Support to remotely toggle push notifications (#2686) 2022-09-22 08:37:54 +00:00
Michael Telatynski 8599a98b47 Fix backpagination at end logic being spec non-conforming (#2680) 2022-09-21 16:35:07 +01:00
renovate[bot] 7e24cb6cae Update jest monorepo to v29.0.3 (#2682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 09:25:43 +01:00
renovate[bot] 38aa8d18c0 Update all (#2684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 09:13:36 +01:00
Germain 2967ee6309 Read receipts for threads (#2635) 2022-09-21 07:50:44 +00:00
renovate[bot] 2e10b6065c Update typescript-eslint monorepo to v5.38.0 (#2685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 08:42:30 +02:00
renovate[bot] 69057ee035 Update babel monorepo to v7.19.1 (#2681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 08:25:16 +02:00
RiotRobot f34e568a98 v19.6.0-rc.1 2022-09-20 13:55:51 +01:00
RiotRobot 6fd80ed3ed Prepare changelog for v19.6.0-rc.1 2022-09-20 13:55:50 +01:00
Kerry 9ff11d1f32 test typescriptification - last few unit test files (#2675)
* renamed:    spec/unit/crypto/verification/sas.spec.js -> spec/unit/crypto/verification/sas.spec.ts

* ts issues in sas.spec

* renamed:    spec/unit/crypto/verification/secret_request.spec.js -> spec/unit/crypto/verification/secret_request.spec.ts

* ts issues in secret_request.spec

* renamed:    spec/unit/crypto/verification/verification_request.spec.js -> spec/unit/crypto/verification/verification_request.spec.ts

* ts fix verification_req.spec

* renamed:    spec/browserify/sync-browserify.spec.js -> spec/browserify/sync-browserify.spec.ts

* fix strict

* formatting
2022-09-16 16:00:40 +00:00
Kerry 41ab3337b5 test typescriptification - spec/unit/crypto/verification (#2673)
* renamed:    spec/unit/crypto/verification/request.spec.js -> spec/unit/crypto/verification/request.spec.ts

* renamed:    spec/unit/crypto/verification/qr_code.spec.js -> spec/unit/crypto/verification/qr_code.spec.ts

* renamed:    spec/unit/crypto/verification/InRoomChannel.spec.js -> spec/unit/crypto/verification/InRoomChannel.spec.ts

* fix ts issues in InRoomChannel.spec

* renamed:    spec/unit/crypto/verification/util.js -> spec/unit/crypto/verification/util.ts

* fix ts issues in util.t

* fix strict errors in util.ts

* js lint
2022-09-16 10:46:56 +02:00
Michael Telatynski 1432e094c2 Update sonarcloud.yml 2022-09-15 14:26:22 +01:00
Michael Telatynski e09936cc9b Update sonarcloud.yml (#2671) 2022-09-15 14:16:15 +01:00
Germain d32d190a8a Change return type to avoid null strict error (#2630) 2022-09-14 09:02:16 +01:00
Michael Telatynski 53de8d5690 Fix reset_dependency for if the dep is indirect (#2664)
Like for in element-desktop
2022-09-14 08:57:52 +01:00
Michael Telatynski 829110b580 Update release-npm.yml (#2665) 2022-09-14 08:57:36 +01:00
RiotRobot 59c82cb679 Resetting package fields for development 2022-09-13 12:32:41 +01:00
RiotRobot 6d3741d55c Merge branch 'master' into develop 2022-09-13 12:32:37 +01:00
RiotRobot 43213fec78 v19.5.0 2022-09-13 12:31:14 +01:00
RiotRobot c8438ff6da Prepare changelog for v19.5.0 2022-09-13 12:31:13 +01:00
RiotRobot a1d0f037e2 Fix release.sh check_dependency 2022-09-13 12:26:03 +01:00
Šimon Brandner fb565f301b Remove support for unstable private read receipts (#2624) 2022-09-12 18:04:14 +02:00
Michael Telatynski afa3b37ad5 Revert "Add login flow types from matrix-react-sdk (#2633)" (#2661)
* Revert "Add login flow types from matrix-react-sdk (#2633)"

This reverts commit 583e4808

* Leave login flow types, only revert method return type

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2022-09-12 11:25:39 +01:00
Hugh Nimmo-Smith a57c430b09 Implementation of MSC3824 to add action= param on SSO login (#2398)
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-11 21:18:33 +00:00
Hugh Nimmo-Smith 583e48086b Add login flow types from matrix-react-sdk (#2633)
Co-authored-by: Faye Duxovni <fayed@element.io>
2022-09-11 21:50:10 +01:00
Robin b22c671fee Add a property aggregating all names of a NamespacedValue (#2656)
For convenience
2022-09-09 09:55:17 -04:00
David Baker 1b86acb2fb Fix import locations (#2655)
These were causing circular references in the group call branch
2022-09-08 20:51:42 +01:00
renovate[bot] d2f7a2575e Update all (major) (#2651)
* Update all

* Pin p-retry once more

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-09-08 09:52:50 +00:00
Michael Telatynski 8490f72488 Tweak backport labels (#2652) 2022-09-07 11:51:11 +00:00
kegsay d87e53858b Merge pull request #2628 from matrix-org/kegan/ss-member-counts
sliding sync: add invited|joined_count
2022-09-07 11:22:47 +02:00
David Teller 917e8c01d8 Base support for MSC3847: Ignore invites with policy rooms (#2626)
* Base support for MSC3847: Ignore invites with policy rooms

Type: enhancement

* Base support for MSC3847: Ignore invites with policy rooms

Type: enhancement

* WIP: Applying feedback

* WIP: Applying feedback

* WIP: CI linter gives me different errors, weird

* WIP: A little more linting
2022-09-06 22:17:42 -06:00
renovate[bot] eb3309db43 Update jest monorepo to v29 (#2649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 22:02:40 -06:00
renovate[bot] 65741d7860 Update all (#2647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 22:05:37 +01:00
renovate[bot] a7a264f4e7 Update typescript-eslint monorepo to v5.36.2 (#2645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 17:53:05 +01:00
renovate[bot] 0fa125b60a Update babel monorepo to v7.19.0 (#2644)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 17:52:57 +01:00
Michael Telatynski 8aee884d03 Fix handling of remote echoes doubling up (#2639)
* Fix handling of remote echoes doubling up

* Simplify code

* Make TSC strict happier

* Update `timelineWasEmpty` to match type

* Add tests

* Add tests

* Add lowly `!`
2022-09-06 13:27:24 +01:00
RiotRobot a8fd0f3d13 Fix release.sh check_dependency order 2022-09-06 12:48:52 +01:00
Michael Telatynski 876491e38d Remove redundant concurrency 2022-09-06 12:36:54 +01:00
RiotRobot 71fcd9f35b v19.5.0-rc.4 2022-09-06 12:35:27 +01:00
RiotRobot 193ff0b4d1 Prepare changelog for v19.5.0-rc.4 2022-09-06 12:35:27 +01:00
RiotRobot f616022d07 Fix self-referencing gha concurrency leading to blocked jobs 2022-09-06 12:34:32 +01:00
RiotRobot 3c206c0b85 v19.5.0-rc.3 2022-09-06 12:25:58 +01:00
RiotRobot a8c4ff473a Prepare changelog for v19.5.0-rc.3 2022-09-06 12:25:57 +01:00
RiotRobot 289a930cda Fix release.sh 2022-09-06 12:25:07 +01:00
RiotRobot 8ba30bc4ef v19.5.0-rc.2 2022-09-06 12:21:02 +01:00
RiotRobot 6e28634819 Prepare changelog for v19.5.0-rc.2 2022-09-06 12:21:01 +01:00
RiotRobot b1e70c5404 Fix release.sh 2022-09-06 12:20:41 +01:00
RiotRobot b11c502a40 Merge branch 'develop' into staging 2022-09-06 12:18:10 +01:00
Michael Telatynski 167f51c8cd Fix release script for layers without a release_config.yaml file (#2642) 2022-09-06 12:17:40 +01:00
RiotRobot 5d2753241e Merge branch 'develop' into staging 2022-09-06 12:11:27 +01:00
Michael Telatynski 274fe447fd Simplify releases: move npm publishing to gha, consolidate scripts (#2616)
* Remove stale comment re dependency

* Move npm publishing from release.sh to GHA

* Extract js-sdk & react-sdk post release steps

* Consolidate release subproject upgrade management
2022-09-06 12:10:26 +01:00
RiotRobot c0f1849a83 v19.5.0-rc.1 2022-09-06 11:28:19 +01:00
RiotRobot 7b2b618d26 Prepare changelog for v19.5.0-rc.1 2022-09-06 11:28:19 +01:00
Kerry 37187ef347 Test typescriptification - room-member and room-state (#2601)
* renamed:    spec/MockStorageApi.js -> spec/MockStorageApi.ts

* renamed:    spec/olm-loader.js -> spec/olm-loader.t

* renamed:    spec/unit/room-state.spec.js -> spec/unit/room-state.spec.ts

* ts fixes in room-state.spec

* renamed:    spec/unit/room-member.spec.js -> spec/unit/room-member.spec.ts

* ts fixes in room-member.spec

* strict mode fixes for MockStorageApi

* strict ts fixes in room-state

* strict errors
2022-09-05 10:38:05 +02:00
3nprob e87ce873b0 utils: Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal (#2586)
* utils: Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal

* Fix bugs in sync test

This test wrongly asserted that `initialSyncLimit` would be used to make a filter
It is used only for the initial sync inline filter, and not in POST /filter

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-09-01 21:36:24 +00:00
Michael Weimann 207171efd6 Link contributing to Element Web (#2621) 2022-09-01 13:49:47 +02:00
Travis Ralston 8cc5efdf46 Update CHANGELOG.md 2022-08-31 11:28:56 -06:00
RiotRobot cbcf47d5c0 Resetting package fields for development 2022-08-31 16:26:39 +01:00
RiotRobot bbaa0e6536 Merge branch 'master' into develop 2022-08-31 16:26:39 +01:00
RiotRobot 1efeb1ec0e v19.4.0 2022-08-31 16:24:27 +01:00
RiotRobot 06e8d98911 Prepare changelog for v19.4.0 2022-08-31 16:24:26 +01:00
Travis Ralston 8716c1ab9b Convert several internal maps to real maps 2022-08-31 09:21:46 -06:00
Kegan Dougal ac7f505a2b Use the right nulls 2022-08-30 18:13:37 +01:00
Kegan Dougal b5576758e4 Even more strict TS type checking 2022-08-30 18:07:53 +01:00
Kegan Dougal c32a83fdac Linting 2022-08-30 18:02:56 +01:00
Kegan Dougal 2d9556c1bb More strict TS type checking 2022-08-30 18:00:03 +01:00
Kegan Dougal 818b70554a Strict TS checks 2022-08-30 17:50:48 +01:00
Kegan Dougal 725336ffc6 sliding sync: add invited|joined_count
This is critical for calculating client-side push rules correctly.
Without it, the push processor may think rooms have a different
number of members, resulting typically in annoying failure modes
where rooms constantly make noises because the code thinks they
are 1:1 rooms.
2022-08-30 17:41:54 +01:00
renovate[bot] 1fbd8983ed Lock file maintenance (#2625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-29 00:00:48 -04:00
Faye Duxovni 1c77816dbd Use deep equality comparisons when searching for outgoing key requests by target (#2623) 2022-08-27 03:13:08 +00:00
Michael Telatynski c1160f40c2 Tweak tsc-strict config (#2620) 2022-08-25 09:49:58 +01:00
Michael Telatynski b789cc5933 Refactor Sync and fix initialSyncLimit (#2587)
* Small tidy-up to sync.ts

* Convert doSync into a while loop

* Apply `initialSyncLimit` only to initial syncs

* Convert matrix-client-syncing spec to TS

* Add tests around initial sync filtering

* Switch confusing filterId field for `filter`

* Tweak doSync error control flow

* Fix error control flow intricacies

* use includes

* Add tests

* Fix some strict mode errors

* Fix more strict mode errors

* Fix some strict mode errors
2022-08-23 16:25:54 +01:00
Jonathan Otto 5e4474b959 Fix room membership race with PREPARED event (#2613)
* Fix room membership race with PREPARED event

See the call site of the original triggering event of this function: https://github.com/matrix-org/matrix-js-sdk/blob/b265d795a427c6d30ccdf279a09f7836509df863/src/sliding-sync.ts#L789-L806

I think the bug is current code assumes downstream event listeners of `SlidingSyncEvent.RoomData` have synchronous execution so that by the time it emits `SlidingSyncState.Complete`, and eventually `SyncState.Prepared` the room state is correct. But since SlidingSyncSdk's `processRoomData` is async, and the membership field was being set after the async, it looks like `SlidingSyncState.Complete` was being fired before the membership field was set.

* Rm whitespace
2022-08-23 15:01:33 +00:00
Kegan Dougal 4059b5bfba Merge branch 'develop' into kegan/sync-v3 2022-08-23 15:57:08 +01:00
Michael Telatynski 8e646ea584 Add static analysis for tsc --strict (#2615)
* Initial attempt at CI to annotate new TSC errors

* Make tsconfig file valid

* enable debug

* Specify commit

* Fix commit specification

* Switch back to main

* Tweak permissions

* Add strict mode failure

* Attempt number two

* Fix ts-extra-args

* Add static analysis for tsc --strict
2022-08-23 14:02:50 +01:00
RiotRobot 528e9343ae v19.4.0-rc.1 2022-08-23 10:53:02 +01:00
RiotRobot 6571b6a1ab Prepare changelog for v19.4.0-rc.1 2022-08-23 10:53:01 +01:00
kegsay 1df329df7c Merge pull request #2610 from matrix-org/kegan/ss-bugfix
sliding sync: handle lone DELETE and INSERT operations
2022-08-23 08:42:34 +01:00
kegsay 760eeaeed7 Merge pull request #2612 from matrix-org/kegan/ss-tags
Add tags and not_tags to the list of valid sliding sync filters
2022-08-23 08:42:14 +01:00
renovate[bot] 438fc70615 Update all (#2614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-23 08:42:56 +02:00
Kegan Dougal de3b3960d2 Add tags and not_tags to the list of valid sliding sync filters 2022-08-22 18:20:51 +01:00
Robin b265d795a4 Re-emit room state events on rooms (#2607)
* Re-emit room state events on rooms

This also fixes some potential memory leaks and abuse of
removeAllListeners in sync.ts.

* Remove some stray whitespace

* Deduplicate some code to appease SonarCloud

* Name helper function more explicitly
2022-08-22 17:04:32 +02:00
Michael Telatynski eb79f6246d Add ability to override built in room name generator for an i18n'able one (#2609)
* Add ability to override built in room name generator for an i18n'able one

* Add tests
2022-08-22 14:39:04 +01:00
Kegan Dougal 37f8f736e0 sliding sync: handle lone DELETE and INSERT operations
If you leave a room you can get a lone DELETE op.
If you join a room you can get a lone INSERT op.

Up until now, we've assumed these operations happen at the ends
of the list (e.g [0] or [length-1]) which is not guaranteed as it
depends on the sort order (e.g sort alphabetically and join a room
called 'D'). In this scenario, the indexes would not be tracked
correctly. Fixed with integration tests.
2022-08-22 14:37:10 +01:00
renovate[bot] 4b1a443f90 Lock file maintenance (#2608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-22 09:30:03 +01:00
Kegan Dougal 3a120f8fb8 Assert pos values as well 2022-08-19 18:19:24 +01:00
Kegan Dougal d2535b8516 Linting 2022-08-19 18:15:33 +01:00
Kegan Dougal 2cda229bc4 Automatically reconnect sessions when sliding sync expires them
This can happen when you close your laptop overnight,
as the server will not hold onto in-memory resources
for your connection indefinitely. When this happen,
the server will HTTP 400 you with "session expired".

At this point, it is no longer safe to remember anything
and you must forget everything and resend any sticky
parameters. This commit does the sticky parameters and
re-establishes the connection, but it may need additional
work to make the JS SDK forget now invalid data.
2022-08-19 17:33:07 +01:00
Šimon Brandner 3ae974e23e Remove duplicate log of answering a call (#2595) 2022-08-17 10:23:06 +00:00
RiotRobot 566b4ba56c Resetting package fields for development 2022-08-16 15:25:58 +01:00
RiotRobot 13291f33d2 Merge branch 'master' into develop 2022-08-16 15:25:57 +01:00
RiotRobot 8502759e3e v19.3.0 2022-08-16 15:23:33 +01:00
RiotRobot 24f9075a84 Prepare changelog for v19.3.0 2022-08-16 15:23:33 +01:00
ElementRobot d18aae09c8 Fix: Handle parsing of a beacon info event without asset (#2591) (#2592)
* test case

* handle missing beacon info asset

* default beacon info asset type to self

* make BeaconLocationState.assetType optional

(cherry picked from commit be3e731499)

Co-authored-by: Kerry <kerrya@element.io>
2022-08-16 14:50:49 +01:00
Kerry be3e731499 Fix: Handle parsing of a beacon info event without asset (#2591)
* test case

* handle missing beacon info asset

* default beacon info asset type to self

* make BeaconLocationState.assetType optional
2022-08-16 15:33:19 +02:00
RiotRobot a9f2ae6b55 v19.3.0-rc.2 2022-08-12 13:24:20 +01:00
RiotRobot b254ca7fc8 Prepare changelog for v19.3.0-rc.2 2022-08-12 13:24:19 +01:00
3nprob 3f6f5b69c7 Improve test coverage and modernize style for interactive-auth (#2574)
* style: address no-mixed-operators errors,minor style improvements

* test: Fix async interactive-auth tests, add test case

* tests: Fix incorrectly stringified mock response

* pushprocessor: style update

* use async primitives in interactive-auth-spec

* lint

* fixup: remove duplicate test

* add test case for no-flow-with-session for interactive-auth

* interactive-auth: handle non-existing error.data

* async test fix

* test: add dummyauth test

* add testing for errcode

* Revert "pushprocessor: style update"

This reverts commit 3ed0fdfb73ae55b725aa7c74d9cab35fb96c9178.

* add testcase for missing error data

* test: move sessionId assignment

* Add tests to improve coverage for interactive-auth

* pushprocessor: style update
2022-08-11 15:29:53 +01:00
ElementRobot 0e8bd3f02d Fix finding event read up to if stable private read receipts is missing (#2585) (#2588)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
(cherry picked from commit 478270b225)

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-08-11 12:36:54 +00:00
Šimon Brandner 478270b225 Fix finding event read up to if stable private read receipts is missing (#2585)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-08-11 13:30:51 +01:00
kegsay 9eb72908a7 Merge pull request #2583 from matrix-org/kegan/sync-v3
sliding sync bugfix: ensure history is treated as history and not live events
2022-08-10 20:00:53 +01:00
Kegan Dougal 2728d74771 Review comments 2022-08-10 19:53:36 +01:00
Kegan Dougal edcef9364c Only add events if there are some; set the pagination token for faster scrollback 2022-08-10 12:43:47 +01:00
Kegan Dougal 1635ac9971 sliding sync bugfix: ensure history is treated as history and not live events
with tests
2022-08-10 12:32:28 +01:00
kegsay 8f13df2dd9 Merge pull request #2567 from matrix-org/kegan/sync-v3
Add txn_id support to sliding sync
2022-08-10 12:16:47 +01:00
Kegan Dougal fa9f078a75 Review comments 2022-08-10 11:53:13 +01:00
Michael Telatynski 055af933cc Update backport.yml 2022-08-10 11:41:44 +01:00
Michael Telatynski 1ba2730e25 Update backport.yml (#2582) 2022-08-10 11:33:51 +01:00
github-actions[bot] 1c9d644a23 Update jsdoc.yml (#2577) (#2581)
(cherry picked from commit 9ee94c9902)

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-08-10 09:31:02 +01:00
Michael Telatynski e29e0d15a5 Set up basic Backporting action (#2580)
* Create backport.yml

* Update backport.yml
2022-08-10 09:21:39 +01:00
Michael Telatynski 9ee94c9902 Update jsdoc.yml (#2577) 2022-08-10 09:04:38 +01:00
renovate[bot] 1645867ea6 Update babel monorepo to v7.18.10 (#2578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-10 07:27:31 +01:00
renovate[bot] 24d4181a08 Update typescript-eslint monorepo to v5.33.0 (#2579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-09 15:42:40 -04:00
RiotRobot f576a9f2e9 v19.3.0-rc.1 2022-08-09 17:03:34 +01:00
RiotRobot fed121b0aa Prepare changelog for v19.3.0-rc.1 2022-08-09 17:03:34 +01:00
3nprob 3e37c74264 Document where new linting rules go (#2573) 2022-08-09 09:59:17 +02:00
Faye Duxovni 3762c20aad Revert "Always block sending keys to unverified devices of verified users (#2562)" (#2571)
This will be rolled out again later with more accompanying UI adjustments, including clearer error messages and possibly the option to disable it per-room.
2022-08-08 12:27:41 -04:00
Kegan Dougal 2596999cb8 Review comments 2022-08-08 14:26:24 +01:00
Kegan Dougal a3248c0aa1 Merge branch 'develop' into kegan/sync-v3 2022-08-08 12:51:03 +01:00
Matthew Hodgson c96f1ba22b Merge pull request #2569 from matrix-org/matthew/avoid-sync-overlaps
Don't load the sync accumulator if there's already a sync persist in flight
2022-08-08 10:18:39 +01:00
Matthew Hodgson 43c81358b2 don't load the sync accumulator if there's already a sync persist in flight
this should hopefully reduce chances of
https://github.com/vector-im/element-web/issues/21541 a bit more
as we were incorrectly loading the sync accumulator even
if a sync persist was already in flight, thus wasting RAM
and increasing the chance of the renderer process OOMing
2022-08-07 01:13:06 +01:00
Kegan Dougal e05f9b5815 Add txn_id support to sliding sync
This allows clients to know when a request has been applied
on the server. This allows us to change `resend(): void` to
`resend(): Promise<string>` which resolves/rejects with the
transaction ID when it has been applied/not.
2022-08-05 17:28:02 +01:00
Šimon Brandner 6316a6ae44 Add support for stable prefixes for MSC2285 (#2524)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-08-05 17:33:49 +02:00
David Baker 575b416856 Simplify encryptAndSendToDevices (#2566)
It went to quite a lot of effort to gather a bunch of information to
return, but the only thing using it already had all that info anyway.
2022-08-05 15:58:33 +01:00
David Baker 7b7f8c1592 Increase timeout to try & avoid flakiness in queueToDevice test (#2565)
https://github.com/matrix-org/matrix-js-sdk/issues/2561
2022-08-05 15:35:29 +01:00
David Baker 3907d1c28f Fix output after test finished (#2564)
* Fix output after test finished

As per comment

* Add more flushes
2022-08-04 17:28:42 +01:00
Robin c629d2f60e Emit an event when the client receives TURN servers (#2529)
* Emit an event when the client receives TURN servers

* Add tests

* Fix lints
2022-08-04 11:44:10 -04:00
Faye Duxovni 43b453804b Always block sending keys to unverified devices of verified users (#2562) 2022-08-04 11:11:12 -04:00
Šimon Brandner d867affc40 Remove stream-replacement (#2551) 2022-08-03 21:45:37 +02:00
Robin c36bfc821c Add support for sending user-defined encrypted to-device messages (#2528)
* Add support for sending user-defined encrypted to-device messages

This is a port of the same change from the robertlong/group-call branch.

* Fix tests

* Expose the method in MatrixClient

* Fix a code smell

* Fix types

* Test the MatrixClient method

* Fix some types in Crypto test suite

* Test the Crypto method

* Fix tests

* Upgrade matrix-mock-request

* Move useRealTimers to afterEach
2022-08-03 16:16:48 +00:00
David Baker 7e784da00a Retry to-device messages (#2549)
* Retry to-device messages

This adds a queueToDevice API alongside sendToDevice which is a
much higher-level API that adds the messages to a queue, stored in
persistent storage, and retries them periodically. Also converts
sending of megolm keys to use the new API.

Other uses of sendToDevice are nopt converted in this PR, but could
be later.

Requires https://github.com/matrix-org/matrix-mock-request/pull/17

* Bump matrix-mock-request

* Add more waits to make indexeddb tests pass

* Switch some test expectations to queueToDevice

* Stop straight away if the client has been stopped

Hopefully will fix tests being flakey and logging after tests have
finished.

* Add return types & fix constant usage

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Stop the client in all test cases

Co-authored-by: Germain <germains@element.io>
2022-08-03 13:32:58 +01:00
Germain b79f469008 Use EventType enum values instead of hardcoded strings (#2557) 2022-08-03 08:54:11 +00:00
RiotRobot cf33569a21 Resetting package fields for development 2022-08-02 17:01:12 +01:00
RiotRobot fb0a0c66c8 Merge branch 'master' into develop 2022-08-02 17:01:11 +01:00
RiotRobot aac0023338 v19.2.0 2022-08-02 16:58:54 +01:00
RiotRobot e3873ddef5 Prepare changelog for v19.2.0 2022-08-02 16:58:53 +01:00
kegsay f0991348e2 Merge pull request #2555 from matrix-org/kegan/sync-v3
Sliding sync: add missing filters from latest MSC
2022-08-02 14:54:37 +01:00
Kegan Dougal fa6708c27e Gracefully handle missing room_ids 2022-08-01 16:34:11 +01:00
Kegan Dougal 4427201326 Sliding sync: add missing filters from latest MSC 2022-08-01 16:30:33 +01:00
Kerry 4a4241806e test typescriptification - autodiscovery / crypto specs (#2550)
* spec/unit/autodiscovery.spec.js -> spec/unit/autodiscovery.spec.ts

* fix ts in autodiscovery.spec

* renamed:    spec/unit/crypto.spec.js -> spec/unit/crypto.spec.ts

* fix ts in crypto.spec

* fix some strict errors
2022-07-29 09:11:01 +00:00
David Baker 3824f65d15 Prevent double mute status changed events (#2502) (#2522)
Audio & video mute status were set in separate calls but share a
mute status changed event, so you'd always get two mute status
changed events emitted. We could suppress events where the mute
status didn't change, but this would still get two events saying
the same thing when they both changed. Instead, merge setAudioMuted
& setVideoMuted into a single call that sets either or both.

Port of https://github.com/matrix-org/matrix-js-sdk/pull/2502 from
group call branch
2022-07-28 16:13:00 +01:00
Michael Telatynski 3c17e4a6d6 Use consolidated Renovate config (#2548)
* Delete renovate.json

* Create renovate.json
2022-07-28 08:13:02 +02:00
Kerry 75513d08de test typescriptification - misc (#2547)
* renamed:    spec/unit/login.spec.js -> spec/unit/login.spec.ts

* type test client

* renamed:    spec/unit/interactive-auth.spec.js -> spec/unit/interactive-auth.spec.ts

* fix ts issues in interactive-auth.spec

* renamed:    spec/unit/filter.spec.js -> spec/unit/filter.spec.ts

* fix ts in filter.spec

* renamed:    spec/unit/event.spec.js -> spec/unit/event.spec.ts

* ts in event.spec

* renamed:    spec/unit/pushprocessor.spec.js -> spec/unit/pushprocessor.spec.ts

* fix ts in pushprocessor.spec

* fix ts in realtime-callbacks.spec

* renamed:    spec/unit/content-repo.spec.js -> spec/unit/content-repo.spec.ts

* fix signature for getHttpUriForMxc

* pr fixes
2022-07-28 08:09:21 +02:00
Šimon Brandner 7cb3b40493 Use stable prefixes for MSC3827 (#2537) 2022-07-27 20:10:39 +02:00
Kerry ab89804c55 test typescriptification: unit/crypto/algorithm specs (#2538)
* typescriptify megolm.spec

* add copyright

* renamed:    spec/unit/crypto/algorithms/olm.spec.js -> spec/unit/crypto/algorithms/olm.spec.ts

* fix ts issues in olm.spec

* remove comment

* more types in megolm and olm specs
2022-07-27 17:43:17 +02:00
renovate[bot] ab6cf93c2b Lock file maintenance (#2546)
* Lock file maintenance

* Empty commit to retry CI

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Robin Townsend <robin@robin.town>
2022-07-27 15:17:00 +00:00
Kerry 4c80762e22 test typescriptification - timeline-window, scheduler, etc (#2539)
* spec/unit/user.spec.js -> spec/unit/user.spec.ts

* fix ts in user.spec

* renamed:    spec/unit/timeline-window.spec.js -> spec/unit/timeline-window.spec.ts

* overdo it fixing types in timeline-window.spec

* renamed spec/unit/sync-accumulator.spec.js spec/unit/sync-accumulator.spec.ts

* fix ts in sync-accumalator.spec

* spec/unit/scheduler.spec.js -> spec/unit/scheduler.spec.ts

* fix ts in scheduler.spec

* missed types in timeline-window spec
2022-07-27 15:10:20 +00:00
Michael Telatynski 1f7e80c68d Require confirmation when doing proper release when intending to make an RC (#2540) 2022-07-27 09:12:57 +01:00
renovate[bot] e91b879a69 Update typescript-eslint monorepo to v5.31.0 (#2544)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-26 17:45:10 +00:00
renovate[bot] 14885ba7a2 Update dependency @types/jest to v28.1.6 (#2543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-26 17:37:13 +00:00
renovate[bot] 0dda187d96 Update all (#2541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-26 17:30:10 +00:00
renovate[bot] 680d8cac4d Update babel monorepo to v7.18.9 (#2542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-26 18:16:53 +01:00
RiotRobot a7fd7fd539 v19.2.0-rc.1 2022-07-26 17:22:49 +01:00
RiotRobot 300d8b026a Prepare changelog for v19.2.0-rc.1 2022-07-26 17:22:48 +01:00
RiotRobot d5a15ac275 Resetting package fields for development 2022-07-26 16:12:26 +01:00
RiotRobot bbb5294b3b Merge branch 'master' into develop 2022-07-26 16:12:25 +01:00
RiotRobot 7731579796 v19.1.0 2022-07-26 16:08:28 +01:00
RiotRobot 55ab38a097 Prepare changelog for v19.1.0 2022-07-26 16:08:27 +01:00
Faye Duxovni 5367ee18fb Re-insert room IDs when decrypting bundled redaction events returned by /sync (#2531) 2022-07-21 10:55:20 +00:00
Faye Duxovni 45db39ec88 Rewrite megolm integration tests with async arrow functions (#2519) 2022-07-21 10:41:46 +00:00
renovate[bot] 32f55de383 Update dependency terser to v5.14.2 [SECURITY] (#2533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-21 08:57:07 +02:00
Hubert Chathi 7e8dfa56d0 Support fixed base64 in SAS verification (#2320) 2022-07-20 09:16:40 -04:00
Faye Duxovni 32bb4b1fc4 Typescriptify megolm integration tests (#2518) 2022-07-14 15:36:34 +00:00
renovate[bot] ae9bb6f27f Lock file maintenance (#2523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-13 15:58:11 +00:00
Travis Ralston 08ab51eeac Remove unstable support for m.room_key.withheld (#2512)
We no longer send or receive the unstable type.
2022-07-13 08:56:01 -06:00
renovate[bot] aa130c88da Update all (major) (#2517)
* Update all

* Pin p-retry due to ESM weirdness

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-07-13 08:16:28 -04:00
Michael Telatynski 9523978861 Update release.sh to support a staging branch (#2514) 2022-07-13 09:43:12 +01:00
Robin 5112340040 Correct the units in TURN servers expiry documentation (#2520)
As shown elsewhere in client.ts, turnServersExpiry really is in
milliseconds rather than seconds. It seems that other libraries like
matrix-react-sdk were already expecting it to be in milliseconds
anyways, so it's just the documentation that was wrong.
2022-07-12 18:48:44 +00:00
Faye Duxovni 6fb40d465e Typescriptify crypto integration tests (#2508) 2022-07-12 12:18:39 -04:00
kegsay 8d7eaa769a Add support for MSC3575: Sliding Sync (#2242)
* sliding sync: add client function and add stub sliding-sync.ts

Mostly c/p from sync.ts. Define interfaces for MSC3575 sliding
sync types. Complete WIP!

* Add core sliding sync classes

* Add integration tests for sliding sync api basics

* gut unused code; add more types

* Use SlidingSync in MatrixClient; stub functions for Sync

Enough to make ele-web actually load okay with 0 rooms.

* Start feeding through room data to the client

* Bugfixes so it sorta ish works

* Refactor the public API for sliding sync

Still needs some work but it's a start.

* Use EventEmitter for callbacks. Add ability to adjust lists and listen for list updates.

- Have atomic getList/setList operations on SlidingSync to update windows etc
- Add a list callback which is invoked with the list indicies and joined count.

* Add stub tests; add listenUntil to make tests easier to read

* No need to resend now

* Add more sliding sync tests; add new setListRanges function

* build tests upon one another to reduce boilerplate and c/p

* More thorough sliding sync tests

* Dependency inject SlidingSync in Client opts when calling startClient()

* Linting

* Fix crash when opts is undefined

* Fix up docs to make CI happy

* Remove all listeners when stop()d to allow for GC

* Add support for extensions

* Add ExtensionE2EE automatically if opts.crypto is present

* Add ExtensionToDevice automatically

* Bugfixes for to_device message processing

* default events to []

* bugfix: don't tightloop when the server is down

Caused by not detecting abort() correctly

* Return null for bad index positions

* Add getListData to get the initial calculated list response

* Add is_tombstoned

* More comments

* Add support for account data extension; rejig extension interface

* Handle invite_state

* Feed through prev_batch tokens

* Linting

* Fix tests

* Linting

* Iterate PR

* Iterate tests and remove unused code

* Update matrix-mock-request

* Make tests happier

* Remove DEBUG/debuglog and use logger.debug

* Update the API to the latest MSC; fixup tests

* Use undefined not null to make it work with the latest changes

* Don't recreate rooms when initial: true

* Add defensive code when unsigned.transaction_id is missing

We can still pair up events by looking at the event_id. We need
to do this in Sliding Sync because the proxy has limitations that
means it cannot guarantee it will always incude a transaction_id
in unsigned. The main reason why is due to the following race condition:
 - A and B are in a DM room.
 - Both are using the proxy.
 - A says "hello".
 - B's sync stream gets "hello" on the proxy. At this point the proxy
   knows it needs to deliver it to A. It does so, but this event has
   no transaction_id as it came down B's sync stream, not A's.
 - If instead, A's sync stream gets "hello" on the proxy, the proxy
   will deliver this message with the transaction_id correctly set.

There are no guarantees that A's sync stream will get the event in a
timely manner, hence the decision to just deliver the events as soon
as the proxy gets the event. This will not be an issue for native
Sliding Sync implementations; this is just a proxy issue.

* Linting

* Add additional sliding sync tests

* Begin adding SlidingSyncSdk tests

* Linting

* Add more sliding sync sdk tests

* Prep work for extension tests

* Linting

* Add account data extension tests

* add to-device tests

* Add E2EE extension tests

* Code smell fixes and extra tests

* Add test for no-txn-id local echo

* Add tests for resolveProfilesToInvites

* Add tests for moving entries down as well as up the list

* Remove conn-management.ts

* Actually verify the event was removed from the txn map

* Handle the case when /sync returns before /send without a txn_id

And ensure all the tests actually test the right things.

* Linting

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-07-12 14:09:58 +00:00
renovate[bot] 7a18991342 Update dependency eslint to v8.19.0 (#2516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-12 13:45:39 +00:00
renovate[bot] f18c64db9e Update typescript-eslint monorepo to v5.30.6 (#2515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-12 13:38:04 +00:00
RiotRobot 7c560b6daa v19.1.0-rc.1 2022-07-12 14:03:09 +01:00
RiotRobot de2add5d5d Prepare changelog for v19.1.0-rc.1 2022-07-12 14:03:08 +01:00
Travis Ralston 24710ee2fc Add a basic PR checklist for all PRs (#2511)
It'll be mildly annoying for core developers who have to constantly remove or edit this, but it'll also serve as a good reminder to do these things.

Note that signoff is not required for core developers.
2022-07-11 14:59:05 -06:00
Šimon Brandner 1fbfdaf221 Don't crash with undefined room in processBeaconEvents() (#2500) 2022-07-11 10:03:44 +02:00
Šimon Brandner c4f7e4d5aa Remove dead code (#2510) 2022-07-11 09:46:50 +02:00
Šimon Brandner 9a6dccb79b Remove setNow from realtime-callbacks.ts (#2509)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-07-10 14:31:48 +02:00
Faye Duxovni 3935152d08 Properly re-insert room ID in bundled thread relation messages from sync (#2505)
Events returned by the `/sync` endpoint, including relations bundled with other events, may have their `room_id`s stripped out. This causes decryption errors if the IDs aren't repopulated.

Fixes vector-im/element-web#22094.
2022-07-08 22:43:38 +00:00
Travis Ralston 72f9a51c27 Actually store the identity server in the client when given as an option (#2503)
* Actually store the identity server in the client when given as an option

* Update requestRegisterEmailToken to a modern spec version too
2022-07-08 01:07:28 -06:00
Travis Ralston efdda8425d Remove MSC3244 support (#2504) 2022-07-08 00:32:27 -06:00
Šimon Brandner 685cab38b9 Improve VoIP integrations testing (#2495) 2022-07-07 08:38:17 +02:00
RiotRobot 85a96c6467 Resetting package fields for development 2022-07-05 14:09:11 +01:00
RiotRobot 2f832a9bfe Merge branch 'master' into develop 2022-07-05 14:09:11 +01:00
RiotRobot 1cb32c174b v19.0.0 2022-07-05 14:06:48 +01:00
RiotRobot b899fd6ccc Prepare changelog for v19.0.0 2022-07-05 14:06:48 +01:00
renovate[bot] f4aecb317f Lock file maintenance (#2491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-04 12:15:41 +00:00
Michael Telatynski ee0264f77d Update pull_request.yaml (#2490) 2022-07-04 10:42:39 +01:00
texuf 9bf8b936d4 Fix return type on funcs in matrixClient to be optionally null (#2488) 2022-07-02 09:11:54 +01:00
Michael Weimann 9f01c8d1fb Expose KNOWN_SAFE_ROOM_VERSION (#2474) 2022-06-30 08:50:14 +02:00
renovate[bot] df5ab4fa91 Update babel monorepo to v7.18.6 (#2477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-29 16:14:04 -06:00
David Baker 39465b50cb Go back to forEach in collectcallstats (#2481)
Older typescript library doesn't know about .values() on the stats
object, so it was failing in react sdk which had an older typescript.
https://github.com/matrix-org/matrix-react-sdk/pull/8935 was an
attempt to upgrade it but did not seem to be helping on CI, despite
being fine locally.
2022-06-29 17:33:09 +01:00
David Baker a745c67dec Fix call.collectCallStats() (#2480)
Regressed by https://github.com/matrix-org/matrix-js-sdk/pull/2352
(you can just use RTCStatsReport as an iterator directly (which
was was what that code was doing before) which uses entries(
which gives you key/value pairs, but using forEach gives you just
the value.
2022-06-29 12:38:48 +01:00
renovate[bot] 55bec4fbe9 Update dependency @types/jest to v28 (#2478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-28 15:53:36 +00:00
renovate[bot] 3a40348860 Update all (#2475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-28 15:52:40 +00:00
renovate[bot] 98262853c7 Update jest monorepo (#2476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-28 15:45:36 +00:00
RiotRobot 4b3aac21db v19.0.0-rc.1 2022-06-28 16:06:58 +01:00
RiotRobot 7521f82cac Prepare changelog for v19.0.0-rc.1 2022-06-28 16:06:58 +01:00
Kerry 9b7628c103 test typescriptification - backup.spec (#2468)
* renamed:    spec/unit/crypto/crypto-utils.js -> spec/unit/crypto/crypto-utils.ts

* ts fixes in crypto-utils

* renamed:    spec/unit/crypto/backup.spec.js -> spec/unit/crypto/backup.spec.ts

* ts fixes in backup.spec

* remove fit

* remove debug
2022-06-27 07:13:18 +00:00
Šimon Brandner 7d2f4cfd42 Send call version 1 as a string (#2471) 2022-06-26 08:43:01 +02:00
Šimon Brandner 5822730797 Implement MSC3827: Filtering of /publicRooms by room type (#2469) 2022-06-20 16:21:46 +02:00
Kerry fc946ab0fa expose latestLocationEvent on beacon model (#2467) 2022-06-17 13:39:23 +02:00
Kerry 9b843daf2f Live location share - add start time leniency (PSF-1081) (#2465)
* remove some of the confusing time travel in beacon.spec

* test cases

* add start time leniency to beacon liveness check
2022-06-16 15:00:45 +02:00
Michael Telatynski ab588f0e51 Fix issue with getEventTimeline returning undefined for thread roots in main timeline (#2454)
* Fix test message utils using overload

* Tweak existing tests

* Add test around `MatrixClient::getEventTimeline`

* Fix test to actually exercise the faulty behaviour

* Extract timelineSet thread belongs logic and test it

* tweak method name
2022-06-15 14:46:08 +00:00
Michael Telatynski b43b4aa9f9 Log real errors and not just their messages, traces are useful (#2464) 2022-06-15 12:44:42 +00:00
Travis Ralston d3ff7655f7 Add missing type property on IAuthData (#2463)
Per spec, for example: https://spec.matrix.org/v1.2/client-server-api/#dummy-auth
2022-06-15 00:37:02 -06:00
Travis Ralston a1ab0d42fe Clearly indicate that lastReply on a Thread can return falsy (#2462) 2022-06-14 16:12:37 -06:00
Michael Telatynski b9ca3ceacd Remove unused sessionStore (#2455)
* Remove unused sessionStorage layer

* Move pending event abstraction into its temporary home

* Add test coverage

* Tweak

* Fix tests mocks

* Add coverage

* Add coverage
2022-06-14 21:29:21 +01:00
Michael Telatynski eb8491c91b Skip running jobs on fork develop where they lack secrets (#2460)
* Skip running jobs on fork `develop` where they lack secrets

* Fix contexts
2022-06-14 11:30:57 +01:00
Jonathan de Jong 78db74dad8 Various changes to src/crypto files for correctness (#2137)
* make various changes for correctness

* apply some review feedback

* Address some review feedback

* add some more correctness

* refactor ensureOutboundSession to fit types better

* change variable naming slightly to prevent confusion

* some wording around exception-catching

* Tidy test

* Simplify

* Add tests

* Add more test coverage

* Apply suggestions from code review

Co-authored-by: Travis Ralston <travpc@gmail.com>

* Update crypto.spec.js

* Update spec/unit/crypto.spec.js

Co-authored-by: Faye Duxovni <duxovni@duxovni.org>

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Travis Ralston <travpc@gmail.com>
Co-authored-by: Faye Duxovni <duxovni@duxovni.org>
2022-06-13 19:05:03 +00:00
Michael Telatynski 4897bccdc9 Improve decryption failure logging (#2453)
* Improve typing

* Log the actual errors to include call stacks
2022-06-13 13:26:01 +01:00
Michael Telatynski aaf508e309 Update sonarcloud.yml (#2452) 2022-06-11 21:02:30 +00:00
Michael Telatynski 8eeefc72e9 Update pull_request.yaml (#2449) 2022-06-10 17:29:48 +01:00
Michael Telatynski 8e896c4da3 Fix issues with getEventTimeline and thread roots (#2444)
* Add additional tests for thread timelines

* Fix issues around mixing up event timeline sets with /context/ API

* Increase coverage

* Increase coverage

* Better scope assertions

* Iterate PR
2022-06-08 22:11:22 +00:00
Kerry 2c2686c910 Test typescriptification - cross-signing.spec (#2441)
* enamed:    spec/unit/crypto/secrets.spec.js -> spec/unit/crypto/secrets.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix ts issues in secrets.spec

Signed-off-by: Kerry Archibald <kerrya@element.io>

* renamed:    spec/unit/crypto/outgoing-room-key-requests.spec.js -> spec/unit/crypto/outgoing-room-key-requests.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix ts issues in outgoing-room-key-requests.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* renamed:    spec/unit/crypto/DeviceList.spec.js -> spec/unit/crypto/DeviceList.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix ts issues in DeviceList.spec

Signed-off-by: Kerry Archibald <kerrya@element.io>

* renamed:    spec/unit/crypto/CrossSigningInfo.spec.js -> spec/unit/crypto/CrossSigningInfo.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix ts issues in CrossSigningInfo.spec

Signed-off-by: Kerry Archibald <kerrya@element.io>

* renamed:    spec/unit/crypto/cross-signing.spec.js -> spec/unit/crypto/cross-signing.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix most function call types in cross-signing.spec

Signed-off-by: Kerry Archibald <kerrya@element.io>

* typed events in cross-signing

Signed-off-by: Kerry Archibald <kerrya@element.io>

* type cross signing keys

Signed-off-by: Kerry Archibald <kerrya@element.io>

* convince the rest of the key types in cross-signing.spec

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use correct type for IDevice
2022-06-08 13:03:02 +00:00
Michael Telatynski b20063f8a8 Pass missing ci secret (#2442)
* Update pull_request.yaml

* Update pull_request.yaml
2022-06-08 13:07:38 +01:00
Michael Telatynski 11cc2aca9d Add CI to improve experience for community (#2439)
* Add CI to improve experience for community

* Fix close-if-fork-develop if-condition

* Extract into reusable workflow

* Update pull_request.yaml
2022-06-08 10:43:36 +00:00
Kerry 8f5162c40d Test typescriptification - crypto unit tests pt 1 (#2440)
* enamed:    spec/unit/crypto/secrets.spec.js -> spec/unit/crypto/secrets.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix ts issues in secrets.spec

Signed-off-by: Kerry Archibald <kerrya@element.io>

* renamed:    spec/unit/crypto/outgoing-room-key-requests.spec.js -> spec/unit/crypto/outgoing-room-key-requests.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix ts issues in outgoing-room-key-requests.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* renamed:    spec/unit/crypto/DeviceList.spec.js -> spec/unit/crypto/DeviceList.spec.ts

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix ts issues in DeviceList.spec

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-06-08 12:39:08 +02:00
Kerry 2982bd79f6 Live location sharing - monitor liveness of beacons yet to start (PSF-1081) (#2437)
* monitor liveness of beacons yet to start

* make watch interval a timeout instead
2022-06-07 17:04:58 +02:00
Eric Eastwood 96c35e2dd3 Add more detail on the context/rationale that should be included when contributing (#2432)
Follow-up to https://github.com/matrix-org/matrix-js-sdk/pull/1933

Spawning from various recent documents and comments:

 - https://github.com/vector-im/element-meta/wiki/Review-process
 - https://github.com/matrix-org/synapse/pull/12846#discussion_r887270734
 - https://gitlab.matrix.org/new-vector/internal/-/wikis/Backend/Reviews
2022-06-07 08:52:08 -05:00
RiotRobot cb5b2e1470 Resetting package fields for development 2022-06-07 12:09:10 +01:00
RiotRobot 37d5e4a5e9 Merge branch 'master' into develop 2022-06-07 12:09:09 +01:00
RiotRobot bf30c15d77 v18.1.0 2022-06-07 12:06:42 +01:00
RiotRobot 8acc92770d Prepare changelog for v18.1.0 2022-06-07 12:06:42 +01:00
Michael Telatynski bfed6edf41 Refactor Relations to not be per-EventTimelineSet (#2412)
* Refactor Relations to not be per-EventTimelineSet

* Fix comment and relations-container init

* Revert timing tweaks

* Fix relations order test

* Add test and simplify thread relations handling

* Fix order of initialising a room object

* Fix test

* Re-add thread handling for relations of unloaded threads

* Ditch confusing experimental getter `MatrixEvent::isThreadRelation`

* Fix room handling in RelationsContainer

* Iterate PR

* Tweak method naming to closer match spec
2022-06-07 11:16:53 +01:00
Michael Telatynski 07189f0637 Add tests for sendEvent threadId handling (#2435)
* Add tests for sendEvent threadId handling

* Fix sendEvent threadId relation support not adding `is_falling_back` field
2022-06-07 09:13:01 +00:00
Jonathan de Jong aa94d5d95c Assume per-user deviceID uniqueness in encryptAndSendKeysToDevices (#2136)
* Segment recorded device info by user ID when tracking key shares.

Fixes #2135.

* address review feedback

* fix userIdDeviceInfo

Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-06-06 15:09:32 +01:00
Michael Telatynski d73126ecb2 Document how to inhibit code coverage requirement (#2436)
on specific sections
2022-06-06 13:13:16 +00:00
Michael Telatynski 76797704ea Move pr_details and sonarqube to released composite actions (#2425)
* Move pr_details and sonarqube to released composite actions

* Modify correct file

* Bring back a reusable workflow for element-web stack sonarqube runs

* Move sonarcloud.yml to the right repo

* Update to matrix-org/sonarcloud-workflow-action@v2.1
2022-06-06 11:37:49 +01:00
Faye Duxovni e35ede0370 The request callback provided by bootstrapCrossSigning is async (#2431) 2022-06-03 08:58:14 -04:00
Kerry 518e16e6d5 matrix-mock-request to 2.0.1 (#2416)
* matrix-mock-request to 2.0.0

Signed-off-by: Kerry Archibald <kerrya@element.io>

* track and destroy timeouts from test client

Signed-off-by: Kerry Archibald <kerrya@element.io>

* remove debug

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix bad property refernce caught by ts TestClient

Signed-off-by: Kerry Archibald <kerrya@element.io>

* Revert "fix bad property refernce caught by ts TestClient"

This reverts commit 92c9f6cb1308fe1afdf0655babcb886acebf05ca.

* update yarn lock

Signed-off-by: Kerry Archibald <kerrya@element.io>

* correct IUploadKeysRequest type

* fix types in TestClient for typed matrix-mock-request

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update to matrix-mock-request 2.0.1

Signed-off-by: Kerry Archibald <kerrya@element.io>
2022-06-03 08:35:26 +00:00
Šimon Brandner 012f6c56e6 Update MSC3786 implementation: Check the state_key (#2429) 2022-06-03 06:06:48 +02:00
Faye Duxovni 8711499121 Don't bug the user while re-checking key backups after decryption failures (#2430) 2022-06-02 13:28:08 -04:00
Eric Eastwood b64dbdce74 Timeline needs to refresh when we see a MSC2716 marker event (#2299)
Inform the client that historical messages were imported in the timeline and they should refresh the timeline in order to see the new events.

Companion `matrix-react-sdk` PR: https://github.com/matrix-org/matrix-react-sdk/pull/8354

The `marker` events are being used as state now because this way they can't be lost in a timeline gap. Regardless of when they were sent, we will still have the latest version of the state to compare against. Any time we see our latest state value change for marker events, prompt the user that the timeline needs to refresh.

> In a [sync meeting with @ara4n](https://docs.google.com/document/d/1KCEmpnGr4J-I8EeaVQ8QJZKBDu53ViI7V62y5BzfXr0/edit#bookmark=id.67nio1ka8znc), we came up with the idea to make the `marker` events as state events. When the client sees that the `m.room.marker` state changed to a different event ID, it can throw away all of the timeline and re-fetch as needed.
>
> For homeservers where the [same problem](https://github.com/matrix-org/matrix-doc/pull/2716#discussion_r782499674) can happen, we probably don't want to throw away the whole timeline but it can go up the `unsigned.replaces_state` chain of the `m.room.marker` state events to get them all.
>
> In terms of state performance, there could be thousands of `marker` events in a room but it's no different than room members joining and leaving over and over like an IRC room.
>
> *-- https://github.com/matrix-org/matrix-spec-proposals/pull/2716#discussion_r782629097*


### Why are we just setting `timlineNeedsRefresh` (and [prompting the user](https://github.com/matrix-org/matrix-react-sdk/pull/8354)) instead of automatically refreshing the timeline for the user?

If we refreshed the timeline automatically, someone could cause your Element client to constantly refresh the timeline by just sending marker events over and over. Granted, you probably want to leave a room like this 🤷. Perhaps also some sort of DOS vector since everyone will be refreshing and hitting the server at the exact same time.

In order to avoid the timeline maybe going blank during the refresh, we could re-fetch the new events first, then replace the timeline. But the points above still stand on why we shouldn't.
2022-06-01 16:31:20 -05:00
Michael Telatynski 2e27a4134c Fix test suite regression due to TestClient refactoring (#2426) 2022-06-01 08:37:44 +00:00
Faye Duxovni 8412ccfa9b Try to load keys from key backup when a message fails to decrypt (#2373)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-06-01 00:43:23 -04:00
RiotRobot c707c8632b v18.1.0-rc.1 2022-05-31 11:28:08 +01:00
RiotRobot 2700f1d053 Prepare changelog for v18.1.0-rc.1 2022-05-31 11:28:07 +01:00
renovate[bot] 142c285063 Lock file maintenance (#2413)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-31 09:52:52 +00:00
Michael Telatynski c0ff9756a8 Update renovate.json (#2419) 2022-05-31 10:43:46 +01:00
Michael Telatynski 64c6c477dc Revert "Fix request, crypto, and bs58 imports (#2414)" (#2422)
This reverts commit 05dd3c4967.
2022-05-30 15:46:59 +01:00
Michael Telatynski c28939c250 Revert "Github Actions pull_request synchronize runs on PR open anyway" (#2420)
* Revert "Github Actions pull_request synchronize runs on PR open anyway (#2418)"

This reverts commit 8c72c5d0e6.

* Update pull_request.yaml
2022-05-30 15:40:50 +01:00
Michael Telatynski 05dd3c4967 Fix request, crypto, and bs58 imports (#2414) 2022-05-30 13:49:25 +00:00
Michael Telatynski 8c72c5d0e6 Github Actions pull_request synchronize runs on PR open anyway (#2418) 2022-05-30 14:28:40 +01:00
renovate[bot] 93293750ce Update babel monorepo (#2409)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-27 16:38:57 +00:00
renovate[bot] 220f709b3a Update typescript-eslint monorepo to v5.26.0 (#2410)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-27 16:32:18 +00:00
Michael Telatynski 6c336ae470 Update sonarcloud.yml (#2411) 2022-05-27 17:28:37 +01:00
renovate[bot] a4a50a4a5c Update jest monorepo (major) (#2407)
* Update jest monorepo

* -w

* Fix guest rooms test to use async/await instead of a done callback

The done callback was never being called because it relies on a `process.nextTick()` deep within the mock. For this test we don't get a "next tick" because the event loop is busy, so we instead cargocult some test infrastructure from surrounding tests and verify the expected API call was cleared from the queue.

* Enable github-actions reporter

* Don't override local reporters

* Stop DeviceLists at end of tests

* stop more clients

* Fix tests and DRY typing

* Fix client/crypto stopping in tests

* Fix Buffer c'tor deprecated warnings

* Fix devicelist-integ test being excluded due to poor naming

Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Travis Ralston <travisr@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-05-27 16:16:00 +01:00
Michael Telatynski 169e865bb6 Fix sonarqube using base branch on fork for detecting new code in pr (#2394)
* Fix sonarqube using base branch on fork for detecting new code in pr

* Add comment

* Tweak comment

* Fix origin vs upstream

* Stop wrongly using github.action_repository

* Fix condition, we can add upstream always
2022-05-27 14:10:15 +01:00
renovate[bot] 8803d2b7e2 Update all (#2408)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-05-27 12:50:51 +00:00
renovate[bot] ab16adfb7d Configure Renovate (#2405)
* Add renovate.json

* Update renovate.json

* Update renovate.json

* Update renovate.json

* Update renovate.json

Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-05-26 20:52:40 +01:00
Michael Telatynski f90adcab89 Fix gha concurrency conditions (#2404) 2022-05-26 09:22:49 +00:00
Michael Telatynski 6090c7bbfc This file is no longer referenced (#2403) 2022-05-26 10:21:27 +01:00
Travis Ralston 12253064d1 Convert getLocalAliases to a stable API call (#2402)
* Convert getLocalAliases to a stable API call

* Appease the linter
2022-05-25 15:56:27 -06:00
Michael Telatynski b2120a0a13 Initial attempt at automating jsdoc (#2382)
* Initial attempt at automating jsdoc

* Commit tested jsdoc workflow
2022-05-25 21:52:24 +01:00
Michael Weimann ad030bfc1f Update relations after every decryption attempt (#2387)
* Update relations after every decryption attempt

If an event is encrypted the aggregation cannot pick up the relation types.
Before this change there was exactly one aggregation retry after decryption.
If the events are being decrypted afterwards (for example on restore
from key backup) the aggregation was not aware of that.
This change adds relation updates after every decryption event if there
has been a decryption error.

Signed-off-by: Michael Weimann <michaelw@matrix.org>
2022-05-25 08:39:18 +02:00
Michael Telatynski 60d665e866 Fix degraded mode for the IDBStore and test it (#2400)
* Add tests around IDB degraded mode

* Fix wrong `this` reference in idb degraded mode store
2022-05-25 07:02:14 +01:00
Michael Telatynski 2fd48a607d Improve PR Details job to use github-script and output labels (#2397)
* Improve PR Details job to use github-script and output labels

* Fix wrongly using github.ref in workflow_run actions which always refer to develop

* Update pr-details to be far more generic
2022-05-24 19:16:08 +01:00
RiotRobot 2f540e31a5 Resetting package fields for development 2022-05-24 12:40:35 +01:00
RiotRobot 7bd9626496 Merge branch 'master' into develop 2022-05-24 12:40:34 +01:00
RiotRobot 58a5742bd3 v18.0.0 2022-05-24 12:36:49 +01:00
RiotRobot d90f983438 Prepare changelog for v18.0.0 2022-05-24 12:36:48 +01:00
Michael Telatynski 81a6e48fc0 Fix http-api MatrixError httpStatus vs http_status (#2396) 2022-05-24 09:05:27 +01:00
Matthew Hodgson db4a6fa9e1 Merge pull request #2250 from matrix-org/matthew/fix-flaky-verif-test
Don't cancel SAS verifications if `ready` is received after `start`
2022-05-23 20:59:39 +01:00
Matthew Hodgson 457f063c67 Merge branch 'develop' into matthew/fix-flaky-verif-test 2022-05-23 20:53:59 +01:00
Šimon Brandner 01eee96b29 Remove dont_notify from the .m.rule.room.server_acl rule (#2395) 2022-05-23 20:45:52 +02:00
Matthew Hodgson 25afb7cb3c Merge pull request #2392 from matrix-org/matthew/fix-sync-acc-overlap
Prevent overlapping sync accumulator persists
2022-05-21 14:32:46 +01:00
Matthew Hodgson c12932b2a6 switch to imperative try...finally 2022-05-21 14:27:07 +01:00
Matthew Hodgson 7ac1d07cc4 don't underscore-prefix private fields 2022-05-21 13:15:52 +01:00
Matthew Hodgson 66adc2d597 prevent overlapping sync accumulator persists
add a flag to stop the sync worker trying to persist to indexeddb
if there are already persists in flight. accumulates user presence
updates in RAM to stop them being lost if the persist is skipped.

hopefully fixes https://github.com/vector-im/element-web/issues/21541
2022-05-21 12:47:59 +01:00
Janne Mareike Koschinski a9516d047f types: improve types for registration calls (#2390) 2022-05-20 16:34:28 +01:00
Michael Telatynski e81d84502b Fix behaviour of isRelation with relation m.replace for state events (#2389)
* Add some short-circuits to skip async code

* Fix behaviour of `isRelation` with relation `m.replace` for state events
2022-05-20 12:32:59 +01:00
RiotRobot 32b2c217c7 v18.0.0-rc.2 2022-05-20 10:03:29 +01:00
RiotRobot 7da555c255 Prepare changelog for v18.0.0-rc.2 2022-05-20 10:03:28 +01:00
Michael Telatynski 88348660eb Catch promise errors in degradable (fixes #2384) (#2385) (#2388)
Co-authored-by: Lars Richard <lars.richard@iserv.eu>
(cherry picked from commit 81d884f899)

Co-authored-by: schmop <lars.richard@rocketmail.com>
2022-05-20 09:56:05 +01:00
schmop 81d884f899 Catch promise errors in degradable (fixes #2384) (#2385)
Co-authored-by: Lars Richard <lars.richard@iserv.eu>
2022-05-19 12:56:25 +01:00
Travis Ralston 2dccefb33a Ensure rooms are recalculated on re-invites (#2374)
* Write the failing test

* Fix the bug

* Add known copyright
2022-05-18 04:37:43 +00:00
RiotRobot af6915b4fc Merge pull request #2381 from matrix-org/actions/upgrade-deps
Upgrade dependencies
2022-05-17 20:09:49 +01:00
t3chguy 3786ea4ca2 [create-pull-request] automated change 2022-05-17 19:03:30 +00:00
Michael Telatynski c7f6777e48 Update CONTRIBUTING.md (#2380) 2022-05-17 18:18:19 +00:00
RiotRobot ba371f7468 v18.0.0-rc.1 2022-05-17 18:26:33 +01:00
RiotRobot 761facf98a Prepare changelog for v18.0.0-rc.1 2022-05-17 18:26:32 +01:00
Michael Telatynski f78fed5ede Revert "Sonarcloud check out upstream develop not fork develop (#2378)" (#2379)
This reverts commit add3732450.
2022-05-17 18:20:42 +01:00
Michael Telatynski add3732450 Sonarcloud check out upstream develop not fork develop (#2378) 2022-05-17 18:09:00 +01:00
Travis Ralston c6af997542 Add a catastrophic throw to thread constructor (#2375)
This is an attempt to narrow down https://github.com/vector-im/element-web/issues/22141
2022-05-17 00:20:41 -06:00
Travis Ralston e9e8e90a94 Remove default push rule override for MSC1930 (#2376)
Folks have had since Matrix 1.0 (June 2019) to upgrade to a compatible server
2022-05-17 00:13:43 -06:00
Johannes Marbach f44510e65f Add support for HTML renderings of room topics (#2272)
* Add support for HTML renderings of room topics

Based on extensible events as defined in [MSC1767]

Relates to: vector-im/element-web#5180
Signed-off-by: Johannes Marbach <johannesm@element.io>

[MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767

* Use correct MSC

* Add overloads for setRoomTopic

* Fix indentation

* Add more tests to pass the quality gate

Co-authored-by: Johannes Marbach <jm@Johanness-Mini.fritz.box>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-05-16 10:37:34 +01:00
Michael Telatynski ba1f6ffc84 Tweak thread creation & event adding to fix bugs around relations (#2369)
* Remove legacy code which caused threads to begin life with too many events

* Update tests & behaviour
2022-05-16 09:01:39 +01:00
Michael Telatynski 3e4f02b41e Update notify-downstream.yaml (#2371) 2022-05-14 01:27:54 -06:00
Michael Telatynski af17fb27b8 Attempt to re-structure workflows to be more generic & reusable (#2364)
* Attempt to re-structure workflows to be more generic & reusable

* Iterate for reusable workflows can't call each other

* don't pass pullrequest params if no prnumber

* Comments

* Fix reusable workflow call

* Pass pr_id properly

* Fix run condition for prdetails job

* Fix needs dependency

* Stash work so far

* Fix copypasta

* Update

* Define outputs from pr_details.yml

* Fix output reporting

* Fix something or other
2022-05-13 23:14:46 +01:00
Michael Telatynski 72013341db More sonar tweaks and typing improvements (#2366)
* More sonar tweaks and typing improvements

* delint

* Write some tests

* Attempt to make TS happy

* Stash tests

* Add tests

* Add `istanbul ignore if` around logging special-case for test env

* Add test

* Comments
2022-05-13 18:08:36 +00:00
Michael Telatynski 6f445ca99a Add stopClient parameter to MatrixClient::logout (#2367)
* Add stopClient parameter to MatrixClient::logout

* Add short-circuit
2022-05-13 19:00:06 +01:00
Michael Telatynski e2af78d8d3 Make pull_request.yaml between the layers consistent and fix enforce labels (#2368) 2022-05-13 18:51:48 +01:00
Michael Telatynski 4721aa1d24 Fix up more types & Sonar warnings (#2363)
* Fix up more types & Sonar warnings

* Fix test

* Add first test for callEventHandler
2022-05-12 10:12:39 +01:00
Michael Telatynski 4d4d6e1411 NodeURL isn't needed as Node exports the standard URL c'tor as global (#2361)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2022-05-11 17:04:10 +01:00
Michael Telatynski 67f5293d6c Tweaks for sonar to correctly report on forked PRs (#2359) 2022-05-11 16:42:25 +01:00
Janne Mareike Koschinski 923ff4b282 registration: add function to re-request email token (#2357) 2022-05-11 13:17:36 +02:00
Michael Telatynski 49dd76b91e Remove redundant checkKey param on isSecretStored (#2360) 2022-05-11 10:53:52 +01:00
Michael Telatynski 34cfa51104 Pass more args to Sonar (#2358)
* Pass projectVersion to Sonar

* Fix s'more

* Fix sonar.lang.patterns.ts

* Apply more tweaks

Based on https://community.sonarsource.com/t/how-to-use-sonarcloud-with-a-forked-repository-on-github/7363/28
2022-05-10 17:11:07 +01:00
Michael Telatynski 685276f056 Pass more args to Sonar (#2356)
* Pass projectVersion to Sonar

* Fix s'more

* Fix sonar.lang.patterns.ts
2022-05-10 16:49:03 +01:00
RiotRobot 03a79dc6dd Resetting package fields for development 2022-05-10 14:50:07 +01:00
RiotRobot 83a4881498 Merge branch 'master' into develop 2022-05-10 14:50:07 +01:00
Travis Ralston 62d77231af Remove spec v1.3 check for threads (#2354)
* Remove spec v1.3 check for threads

Citation: https://matrix.to/#/!ewdjhNcPcEmYNKzlWp:t2l.io/$CkPuvKdFZyFL547JCy5J3MfvLaWUo_a1XEdmiop1PKc?via=matrix.org&via=element.io&via=envs.net

* Enable stable support always for threads

* Fix tests differently
2022-05-09 16:11:04 -06:00
Michael Telatynski 706b4d6054 Improve typing (#2352)
* Fix typing of the store interface

* Fix typed s'more

* re-add check

* Be less dumb

* arg

* Fix types
2022-05-09 11:58:52 +01:00
Šimon Brandner da69ca215b Implement changes to MSC2285 (private read receipts) (#2221)
* Add `ReceiptType`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Implement changes to MSC2285

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Improve tests

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Apply suggestions from review

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Update `getEventReadUpTo()` to handle private read receipts

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Write tests for `getEventReadUpTo()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Give `getReadReceiptForUserId()` a JSDOC

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Types!

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Try to use receipt `ts`s

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-05-06 21:32:41 +02:00
Travis Ralston 95a94cdbe3 Remove hacky custom status feature (#2350)
This is unstable, so should be more than safe to just outright remove without notice.
2022-05-06 13:20:54 -06:00
Michael Telatynski 6b5f4aa0a9 Prune both clear & wire content on redaction (#2346) 2022-05-05 07:14:23 +01:00
Michael Telatynski dea3f52fe9 Another SonarQube happiness pass (#2347) 2022-05-04 21:34:21 -06:00
Michael Telatynski a388fde3e2 Tweak sonar-project.properties (#2348) 2022-05-04 15:36:00 -04:00
Šimon Brandner 1cde686a13 MSC3786: Add a default push rule to ignore m.room.server_acl events (#2333)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-05-04 16:22:43 +02:00
Michael Telatynski 592f2931dc Add label to dependency upgrade PRs (#2345) 2022-05-04 14:27:42 +01:00
Michael Telatynski 030fcb57a5 Update upgrade_dependencies.yml (#2343) 2022-05-03 22:44:57 +01:00
Michael Telatynski 8be30acb11 Apply suggestions from SonarQube (#2340) 2022-05-03 15:40:57 -06:00
github-actions[bot] b86630f0e3 Upgrade dependencies (#2342)
Co-authored-by: t3chguy <t3chguy@users.noreply.github.com>
2022-05-03 22:10:41 +01:00
Michael Telatynski 81c89f69c5 Create manual action for upgrading dependencies after rc cut (#2341) 2022-05-03 21:54:18 +01:00
Michael Telatynski 9b633251d5 Add README badges (#2335) 2022-05-03 14:52:44 -06:00
Matthew Hodgson d9f0704048 reduce flakiness of e2e verif test
it's completely valid to receive a `ready` event after having received a
`start` event as messages may be received or decrypted in any order.

partial (but possibly sufficient?) fix for https://github.com/vector-im/element-web/issues/21488
2022-03-20 20:11:15 +00:00
222 changed files with 31045 additions and 17280 deletions
+29 -1
View File
@@ -1,14 +1,24 @@
module.exports = {
plugins: [
"matrix-org",
"import",
],
extends: [
"plugin:matrix-org/babel",
"plugin:import/typescript",
],
env: {
browser: true,
node: true,
},
settings: {
"import/resolver": {
typescript: true,
node: true,
},
},
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
"no-var": ["warn"],
"prefer-rest-params": ["warn"],
@@ -33,7 +43,19 @@ module.exports = {
"no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", "events"],
"no-restricted-imports": ["error", {
name: "events",
message: "Please use TypedEventEmitter instead"
}],
"import/no-restricted-paths": ["error", {
"zones": [{
"target": "./src/",
"from": "./src/index.ts",
"message": "The package index is dynamic between src and lib depending on " +
"whether release or development, target the specific module or matrix.ts instead",
}],
}],
},
overrides: [{
files: [
@@ -52,6 +74,12 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// The non-TypeScript rule produces false positives
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"],
"quotes": "off",
// We use a `logger` intermediary module
+10 -4
View File
@@ -1,7 +1,13 @@
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
## Checklist
<!-- To specify text for the changelog entry (otherwise the PR title will be used):
Notes:
* [ ] Tests written for new code (and old code if feasible)
* [ ] Linter and other CI checks pass
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
<!--
If you would like to specify text for the changelog entry other than your PR title, add the following:
Notes: Add super cool feature
-->
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>matrix-org/renovate-config-element-web"
]
}
+30
View File
@@ -0,0 +1,30 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
branches:
- develop
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: tibdex/backport@v2
with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+17 -4
View File
@@ -2,13 +2,26 @@ name: Notify Downstream Projects
on:
push:
branches: [ develop ]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
notify-matrix-react-sdk:
notify-downstream:
# Only respect triggers from our develop branch, ignore that of forks
if: github.repository == 'matrix-org/matrix-js-sdk'
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- repo: vector-im/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
runs-on: ubuntu-latest
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@v1
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: vector-im/element-web
event-type: upstream-sdk-notify
repository: ${{ matrix.repo }}
event-type: ${{ matrix.event }}
+74 -7
View File
@@ -1,7 +1,18 @@
name: Pull Request
on:
pull_request_target:
types: [ opened, edited, labeled, unlabeled ]
types: [ opened, edited, labeled, unlabeled, synchronize ]
workflow_call:
inputs:
labels:
type: string
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
required: false
description: "No longer used, uses allchange logic now, will be removed at a later date"
secrets:
ELEMENT_BOT_TOKEN:
required: true
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
jobs:
changelog:
name: Preview Changelog
@@ -10,15 +21,71 @@ jobs:
- uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
requireLabel: true
enforce-label:
name: Enforce Labels
prevent-blocked:
name: Prevent Blocked
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: yogevbd/enforce-label-action@2.1.0
- name: Add notice
uses: actions/github-script@v6
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
REQUIRED_LABELS_ANY: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
BANNED_LABELS: "X-Blocked"
BANNED_LABELS_DESCRIPTION: "Preventing merge whilst PR is marked blocked!"
script: |
core.setFailed("Preventing merge whilst PR is marked blocked!");
community-prs:
name: Label Community PRs
runs-on: ubuntu-latest
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@v1
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
organization: matrix-org
team: Core Team
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
uses: actions/github-script@v6
with:
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Z-Community-PR']
});
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-latest
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
});
github.rest.pulls.update({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
});
+41
View File
@@ -0,0 +1,41 @@
# Must only be called from `release#published` triggers
name: Publish to npm
on:
workflow_call:
secrets:
NPM_TOKEN:
required: true
jobs:
npm:
name: Publish to npm
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
registry-url: 'https://registry.npmjs.org'
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
tag: next
- name: 🎖️ Add `latest` dist-tag to final releases
if: github.event.release.prerelease == false
run: |
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
env:
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.version }}
+58
View File
@@ -0,0 +1,58 @@
name: Release Process
on:
release:
types: [ published ]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
jsdoc:
name: Publish Documentation
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
- name: 📖 Generate JSDoc
run: "yarn gendoc"
- name: 📋 Copy to temp
run: |
tag="${{ github.ref_name }}"
version="${tag#v}"
echo "VERSION=$version" >> $GITHUB_ENV
cp -a "./.jsdoc/matrix-js-sdk/$version" $RUNNER_TEMP
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v3
with:
ref: gh-pages
- name: 🔪 Prepare
run: |
cp -a "$RUNNER_TEMP/$VERSION" .
# Add the new directory to the index if it isn't there already
if ! grep -q ">Version $VERSION</a>" index.html; then
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
fi
- name: 🚀 Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
keep_files: true
publish_dir: .
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+45
View File
@@ -0,0 +1,45 @@
# Must only be called from a workflow_run in the context of the upstream repo
name: SonarCloud
on:
workflow_call:
secrets:
SONAR_TOKEN:
required: true
jobs:
sonarqube:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.2
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
version_cmd: 'cat package.json | jq -r .version'
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
coverage_run_id: ${{ github.event.workflow_run.id }}
coverage_workflow_name: tests.yml
coverage_extract_path: coverage
- uses: Sibz/github-status-action@v1
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+7 -39
View File
@@ -4,44 +4,12 @@ on:
workflows: [ "Tests" ]
types:
- completed
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
sonarqube:
name: SonarQube
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: Download Coverage Report
uses: actions/github-script@v3.1.0
with:
script: |
const artifacts = await github.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }},
});
const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "coverage"
})[0];
const download = await github.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
const fs = require('fs');
fs.writeFileSync('${{github.workspace}}/coverage.zip', Buffer.from(download.data));
- name: Extract Coverage Report
run: unzip -d coverage coverage.zip && rm coverage.zip
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
name: 🩻 SonarQube
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+51 -3
View File
@@ -3,12 +3,15 @@ on:
pull_request: { }
push:
branches: [ develop, master ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
@@ -20,11 +23,21 @@ jobs:
- name: Typecheck
run: "yarn run lint:types"
- name: Switch js-sdk to release mode
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Typecheck (release mode)
run: "yarn run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
@@ -40,7 +53,7 @@ jobs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
@@ -51,3 +64,38 @@ jobs:
- name: Generate Docs
run: "yarn run gendoc"
tsc-strict:
name: Typescript Strict Error Checker
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
checks: write
steps:
- uses: actions/checkout@v3
- name: Get diff lines
id: diff
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with:
include: '["\\.tsx?$"]'
- name: Detecting files changed
id: files
uses: futuratrepadeira/changed-files@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$'
- uses: t3chguy/typescript-check-action@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: false
check-fail-mode: added
output-behaviour: annotate
ts-extra-args: '--strict'
files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }}
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
+7 -4
View File
@@ -2,14 +2,17 @@ name: Tests
on:
pull_request: { }
push:
branches: [ develop, main, master ]
branches: [ develop, master ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
jest:
name: Jest
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Yarn cache
uses: actions/setup-node@v3
@@ -23,10 +26,10 @@ jobs:
run: "yarn build"
- name: Run tests with coverage
run: "yarn coverage --ci"
run: "yarn coverage --ci --reporters github-actions"
- name: Upload Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
@@ -0,0 +1,38 @@
name: Upgrade Dependencies
on:
workflow_dispatch: { }
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: true
jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- name: Upgrade
run: yarn upgrade && yarn install
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/upgrade-deps
delete-branch: true
title: Upgrade dependencies
labels: |
Dependencies
T-Task
- name: Enable automerge
uses: peter-evans/enable-pull-request-automerge@v2
if: steps.cpr.outputs.pull-request-operation == 'created'
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
-2
View File
@@ -1,2 +0,0 @@
instrumentation:
compact: false
+224
View File
@@ -1,3 +1,227 @@
Changes in [21.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.0) (2022-10-25)
==================================================================================================
## 🚨 BREAKING CHANGES
* Changes the `uploadContent` API, kills off `request` and `browser-request` in favour of `fetch`, removed callback support on a lot of the methods, adds a lot of tests. ([\#2719](https://github.com/matrix-org/matrix-js-sdk/pull/2719)). Fixes #2415 and #801.
* Remove deprecated `m.room.aliases` references ([\#2759](https://github.com/matrix-org/matrix-js-sdk/pull/2759)). Fixes vector-im/element-web#12680.
## ✨ Features
* Remove node-specific crypto bits, use Node 16's WebCrypto ([\#2762](https://github.com/matrix-org/matrix-js-sdk/pull/2762)). Fixes #2760.
* Export types for MatrixEvent and Room emitted events, and make event handler map types stricter ([\#2750](https://github.com/matrix-org/matrix-js-sdk/pull/2750)). Contributed by @stas-demydiuk.
* Use even more stable calls to `/room_keys` ([\#2746](https://github.com/matrix-org/matrix-js-sdk/pull/2746)).
* Upgrade to Olm 3.2.13 which has been repackaged to support Node 18 ([\#2744](https://github.com/matrix-org/matrix-js-sdk/pull/2744)).
* Fix `power_level_content_override` type ([\#2741](https://github.com/matrix-org/matrix-js-sdk/pull/2741)).
* Add custom notification handling for MSC3401 call events ([\#2720](https://github.com/matrix-org/matrix-js-sdk/pull/2720)).
* Add support for unread thread notifications ([\#2726](https://github.com/matrix-org/matrix-js-sdk/pull/2726)).
* Load Thread List with server-side assistance (MSC3856) ([\#2602](https://github.com/matrix-org/matrix-js-sdk/pull/2602)).
* Use stable calls to `/room_keys` ([\#2729](https://github.com/matrix-org/matrix-js-sdk/pull/2729)). Fixes vector-im/element-web#22839.
## 🐛 Bug Fixes
* Fix POST data not being passed for registerWithIdentityServer ([\#2769](https://github.com/matrix-org/matrix-js-sdk/pull/2769)). Fixes matrix-org/element-web-rageshakes#16206.
* Fix IdentityPrefix.V2 containing spurious `/api` ([\#2761](https://github.com/matrix-org/matrix-js-sdk/pull/2761)). Fixes vector-im/element-web#23505.
* Always send back an httpStatus property if one is known ([\#2753](https://github.com/matrix-org/matrix-js-sdk/pull/2753)).
* Check for AbortError, not any generic connection error, to avoid tightlooping ([\#2752](https://github.com/matrix-org/matrix-js-sdk/pull/2752)).
* Correct the dir parameter of MSC3715 ([\#2745](https://github.com/matrix-org/matrix-js-sdk/pull/2745)). Contributed by @dhenneke.
* Fix sync init when thread unread notif is not supported ([\#2739](https://github.com/matrix-org/matrix-js-sdk/pull/2739)). Fixes vector-im/element-web#23435.
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
Changes in [20.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0) (2022-10-11)
============================================================================================================
## ✨ Features
* Add local notification settings capability ([\#2700](https://github.com/matrix-org/matrix-js-sdk/pull/2700)).
* Implementation of MSC3882 login token request ([\#2687](https://github.com/matrix-org/matrix-js-sdk/pull/2687)). Contributed by @hughns.
* Typings for MSC2965 OIDC provider discovery ([\#2424](https://github.com/matrix-org/matrix-js-sdk/pull/2424)). Contributed by @hughns.
* Support to remotely toggle push notifications ([\#2686](https://github.com/matrix-org/matrix-js-sdk/pull/2686)).
* Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)).
## 🐛 Bug Fixes
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
* Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee.
* Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377.
* Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784.
Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30)
==================================================================================================
## 🐛 Bug Fixes
* Fix issue in sync when crypto is not supported by client ([\#2715](https://github.com/matrix-org/matrix-js-sdk/pull/2715)). Contributed by @stas-demydiuk.
Changes in [20.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.1) (2022-09-28)
==================================================================================================
## 🐛 Bug Fixes
* Fix missing return when receiving an invitation without shared history ([\#2710](https://github.com/matrix-org/matrix-js-sdk/pull/2710)).
Changes in [20.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.0) (2022-09-28)
==================================================================================================
## 🚨 BREAKING CHANGES
* Bump IDB crypto store version ([\#2705](https://github.com/matrix-org/matrix-js-sdk/pull/2705)).
Changes in [19.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.7.0) (2022-09-28)
==================================================================================================
## 🔒 Security
* Fix for [CVE-2022-39249](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39249)
* Fix for [CVE-2022-39250](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39250)
* Fix for [CVE-2022-39251](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39251)
* Fix for [CVE-2022-39236](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39236)
Changes in [19.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.6.0) (2022-09-27)
==================================================================================================
## ✨ Features
* Add a property aggregating all names of a NamespacedValue ([\#2656](https://github.com/matrix-org/matrix-js-sdk/pull/2656)).
* Implementation of MSC3824 to add action= param on SSO login ([\#2398](https://github.com/matrix-org/matrix-js-sdk/pull/2398)). Contributed by @hughns.
* Add invited_count and joined_count to sliding sync room responses. ([\#2628](https://github.com/matrix-org/matrix-js-sdk/pull/2628)).
* Base support for MSC3847: Ignore invites with policy rooms ([\#2626](https://github.com/matrix-org/matrix-js-sdk/pull/2626)). Contributed by @Yoric.
## 🐛 Bug Fixes
* Fix handling of remote echoes doubling up ([\#2639](https://github.com/matrix-org/matrix-js-sdk/pull/2639)). Fixes #2618.
Changes in [19.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.5.0) (2022-09-13)
==================================================================================================
## 🐛 Bug Fixes
* Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal ([\#2586](https://github.com/matrix-org/matrix-js-sdk/pull/2586)). Contributed by @3nprob.
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
Changes in [19.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.4.0) (2022-08-31)
==================================================================================================
## 🔒 Security
* Fix for [CVE-2022-36059](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D36059)
Find more details at https://matrix.org/blog/2022/08/31/security-releases-matrix-js-sdk-19-4-0-and-matrix-react-sdk-3-53-0
## ✨ Features
* Re-emit room state events on rooms ([\#2607](https://github.com/matrix-org/matrix-js-sdk/pull/2607)).
* Add ability to override built in room name generator for an i18n'able one ([\#2609](https://github.com/matrix-org/matrix-js-sdk/pull/2609)).
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
## 🐛 Bug Fixes
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
* fixed a sliding sync bug which could cause the `roomIndexToRoomId` map to be incorrect when a new room is added in the middle of the list or when an existing room is deleted from the middle of the list. ([\#2610](https://github.com/matrix-org/matrix-js-sdk/pull/2610)).
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078. Contributed by @kerryarchibald.
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
* fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
==================================================================================================
## ✨ Features
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
* Emit an event when the client receives TURN servers ([\#2529](https://github.com/matrix-org/matrix-js-sdk/pull/2529)).
* Add support for stable prefixes for MSC2285 ([\#2524](https://github.com/matrix-org/matrix-js-sdk/pull/2524)).
* Remove stream-replacement ([\#2551](https://github.com/matrix-org/matrix-js-sdk/pull/2551)).
* Add support for sending user-defined encrypted to-device messages ([\#2528](https://github.com/matrix-org/matrix-js-sdk/pull/2528)).
* Retry to-device messages ([\#2549](https://github.com/matrix-org/matrix-js-sdk/pull/2549)). Fixes vector-im/element-web#12851.
* Sliding sync: add missing filters from latest MSC ([\#2555](https://github.com/matrix-org/matrix-js-sdk/pull/2555)).
* Use stable prefixes for MSC3827 ([\#2537](https://github.com/matrix-org/matrix-js-sdk/pull/2537)).
## 🐛 Bug Fixes
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078.
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
* Fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
* Don't load the sync accumulator if there's already a sync persist in flight ([\#2569](https://github.com/matrix-org/matrix-js-sdk/pull/2569)).
Changes in [19.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.2.0) (2022-08-02)
==================================================================================================
## 🦖 Deprecations
* Remove unstable support for `m.room_key.withheld` ([\#2512](https://github.com/matrix-org/matrix-js-sdk/pull/2512)). Fixes #2233.
## ✨ Features
* Sliding sync: add missing filters from latest MSC ([\#2555](https://github.com/matrix-org/matrix-js-sdk/pull/2555)).
* Use stable prefixes for MSC3827 ([\#2537](https://github.com/matrix-org/matrix-js-sdk/pull/2537)).
* Add support for MSC3575: Sliding Sync ([\#2242](https://github.com/matrix-org/matrix-js-sdk/pull/2242)).
## 🐛 Bug Fixes
* Correct the units in TURN servers expiry documentation ([\#2520](https://github.com/matrix-org/matrix-js-sdk/pull/2520)).
* Re-insert room IDs when decrypting bundled redaction events returned by `/sync` ([\#2531](https://github.com/matrix-org/matrix-js-sdk/pull/2531)). Contributed by @duxovni.
Changes in [19.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.1.0) (2022-07-26)
==================================================================================================
## 🦖 Deprecations
* Remove MSC3244 support ([\#2504](https://github.com/matrix-org/matrix-js-sdk/pull/2504)).
## ✨ Features
* `room` now exports `KNOWN_SAFE_ROOM_VERSION` ([\#2474](https://github.com/matrix-org/matrix-js-sdk/pull/2474)).
## 🐛 Bug Fixes
* Don't crash with undefined room in `processBeaconEvents()` ([\#2500](https://github.com/matrix-org/matrix-js-sdk/pull/2500)). Fixes #2494.
* Properly re-insert room ID in bundled thread relation messages from sync ([\#2505](https://github.com/matrix-org/matrix-js-sdk/pull/2505)). Fixes vector-im/element-web#22094. Contributed by @duxovni.
* Actually store the identity server in the client when given as an option ([\#2503](https://github.com/matrix-org/matrix-js-sdk/pull/2503)). Fixes vector-im/element-web#22757.
* Fix call.collectCallStats() ([\#2480](https://github.com/matrix-org/matrix-js-sdk/pull/2480)).
Changes in [19.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.0.0) (2022-07-05)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove unused sessionStore ([\#2455](https://github.com/matrix-org/matrix-js-sdk/pull/2455)).
## ✨ Features
* Implement MSC3827: Filtering of `/publicRooms` by room type ([\#2469](https://github.com/matrix-org/matrix-js-sdk/pull/2469)).
* expose latestLocationEvent on beacon model ([\#2467](https://github.com/matrix-org/matrix-js-sdk/pull/2467)). Contributed by @kerryarchibald.
* Live location share - add start time leniency ([\#2465](https://github.com/matrix-org/matrix-js-sdk/pull/2465)). Contributed by @kerryarchibald.
* Log real errors and not just their messages, traces are useful ([\#2464](https://github.com/matrix-org/matrix-js-sdk/pull/2464)).
* Various changes to `src/crypto` files for correctness ([\#2137](https://github.com/matrix-org/matrix-js-sdk/pull/2137)). Contributed by @ShadowJonathan.
* Update MSC3786 implementation: Check the `state_key` ([\#2429](https://github.com/matrix-org/matrix-js-sdk/pull/2429)).
* Timeline needs to refresh when we see a MSC2716 marker event ([\#2299](https://github.com/matrix-org/matrix-js-sdk/pull/2299)). Contributed by @MadLittleMods.
* Try to load keys from key backup when a message fails to decrypt ([\#2373](https://github.com/matrix-org/matrix-js-sdk/pull/2373)). Fixes vector-im/element-web#21026. Contributed by @duxovni.
## 🐛 Bug Fixes
* Send call version `1` as a string ([\#2471](https://github.com/matrix-org/matrix-js-sdk/pull/2471)). Fixes vector-im/element-web#22629.
* Fix issue with `getEventTimeline` returning undefined for thread roots in main timeline ([\#2454](https://github.com/matrix-org/matrix-js-sdk/pull/2454)). Fixes vector-im/element-web#22539.
* Add missing `type` property on `IAuthData` ([\#2463](https://github.com/matrix-org/matrix-js-sdk/pull/2463)).
* Clearly indicate that `lastReply` on a Thread can return falsy ([\#2462](https://github.com/matrix-org/matrix-js-sdk/pull/2462)).
* Fix issues with getEventTimeline and thread roots ([\#2444](https://github.com/matrix-org/matrix-js-sdk/pull/2444)). Fixes vector-im/element-web#21613.
* Live location sharing - monitor liveness of beacons yet to start ([\#2437](https://github.com/matrix-org/matrix-js-sdk/pull/2437)). Contributed by @kerryarchibald.
* Refactor Relations to not be per-EventTimelineSet ([\#2412](https://github.com/matrix-org/matrix-js-sdk/pull/2412)). Fixes #2399 and vector-im/element-web#22298.
* Add tests for sendEvent threadId handling ([\#2435](https://github.com/matrix-org/matrix-js-sdk/pull/2435)). Fixes vector-im/element-web#22433.
* Make sure `encryptAndSendKeysToDevices` assumes devices are unique per-user. ([\#2136](https://github.com/matrix-org/matrix-js-sdk/pull/2136)). Fixes #2135. Contributed by @ShadowJonathan.
* Don't bug the user while re-checking key backups after decryption failures ([\#2430](https://github.com/matrix-org/matrix-js-sdk/pull/2430)). Fixes vector-im/element-web#22416. Contributed by @duxovni.
Changes in [18.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.1.0) (2022-06-07)
==================================================================================================
## ✨ Features
* Convert `getLocalAliases` to a stable API call ([\#2402](https://github.com/matrix-org/matrix-js-sdk/pull/2402)).
## 🐛 Bug Fixes
* Fix request, crypto, and bs58 imports ([\#2414](https://github.com/matrix-org/matrix-js-sdk/pull/2414)). Fixes #2415.
* Update relations after every decryption attempt ([\#2387](https://github.com/matrix-org/matrix-js-sdk/pull/2387)). Fixes vector-im/element-web#22258. Contributed by @weeman1337.
* Fix degraded mode for the IDBStore and test it ([\#2400](https://github.com/matrix-org/matrix-js-sdk/pull/2400)). Fixes matrix-org/element-web-rageshakes#13170.
* Don't cancel SAS verifications if `ready` is received after `start` ([\#2250](https://github.com/matrix-org/matrix-js-sdk/pull/2250)).
* Prevent overlapping sync accumulator persists ([\#2392](https://github.com/matrix-org/matrix-js-sdk/pull/2392)). Fixes vector-im/element-web#21541.
* Fix behaviour of isRelation with relation m.replace for state events ([\#2389](https://github.com/matrix-org/matrix-js-sdk/pull/2389)). Fixes vector-im/element-web#22280.
* Fixes #2384 ([\#2385](https://github.com/matrix-org/matrix-js-sdk/pull/2385)). Fixes undefined/matrix-js-sdk#2384. Contributed by @schmop.
* Ensure rooms are recalculated on re-invites ([\#2374](https://github.com/matrix-org/matrix-js-sdk/pull/2374)). Fixes vector-im/element-web#22106.
Changes in [18.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v18.0.0) (2022-05-24)
==================================================================================================
## 🚨 BREAKING CHANGES (to experimental methods)
* Implement changes to MSC2285 (private read receipts) ([\#2221](https://github.com/matrix-org/matrix-js-sdk/pull/2221)).
## ✨ Features
* Add support for HTML renderings of room topics ([\#2272](https://github.com/matrix-org/matrix-js-sdk/pull/2272)).
* Add stopClient parameter to MatrixClient::logout ([\#2367](https://github.com/matrix-org/matrix-js-sdk/pull/2367)).
* registration: add function to re-request email token ([\#2357](https://github.com/matrix-org/matrix-js-sdk/pull/2357)).
* Remove hacky custom status feature ([\#2350](https://github.com/matrix-org/matrix-js-sdk/pull/2350)).
## 🐛 Bug Fixes
* Remove default push rule override for MSC1930 ([\#2376](https://github.com/matrix-org/matrix-js-sdk/pull/2376)). Fixes vector-im/element-web#15439.
* Tweak thread creation & event adding to fix bugs around relations ([\#2369](https://github.com/matrix-org/matrix-js-sdk/pull/2369)). Fixes vector-im/element-web#22162 and vector-im/element-web#22180.
* Prune both clear & wire content on redaction ([\#2346](https://github.com/matrix-org/matrix-js-sdk/pull/2346)). Fixes vector-im/element-web#21929.
* MSC3786: Add a default push rule to ignore `m.room.server_acl` events ([\#2333](https://github.com/matrix-org/matrix-js-sdk/pull/2333)). Fixes vector-im/element-web#20788.
Changes in [17.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v17.2.0) (2022-05-10)
==================================================================================================
+1 -253
View File
@@ -1,257 +1,5 @@
Contributing code to matrix-js-sdk
==================================
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)).
How to contribute
-----------------
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description:
* A changelog entry in the `Notes` section (see below)
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
* Describe the why and what is changing in the PR description so it's easy for
onlookers and reviewers to onboard and context switch.
* Include both **before** and **after** screenshots to easily compare and discuss
what's changing.
* Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change.
* Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it.
Things that should *not* go into your PR description:
* Any information on how the code works or why you chose to do it the way
you did. If this isn't obvious from your code, you haven't written enough
comments.
We rely on information in pull request to populate the information that goes
into the changelogs our users see, both for the JS SDK itself and also for some
projects based on it. This is picked up from both labels on the pull request and
the `Notes:` annotation in the description. By default, the PR title will be
used for the changelog entry, but you can specify more options, as follows.
To add a longer, more detailed description of the change for the changelog:
*Fix llama herding bug*
```
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
```
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
the default for PRs labelled with `T-Task`):
*Remove outdated comment from `Ungulates.ts`*
```
Notes: none
```
Sometimes, you're fixing a bug in a downstream project, in which case you want
an entry in that project's changelog. You can do that too:
*Fix another herding bug*
```
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
```
This example is for Element Web. You can specify:
* matrix-react-sdk
* element-web
* element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
to specify in the notes that it's a breaking change - this will be added
automatically based on the label - but remember to tell the developer how to
migrate:
*Remove legacy class*
```
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
```
Other metadata can be added using labels.
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
* `T-Defect`: A bug fix (in either code or docs).
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
If you don't have permission to add labels, your PR reviewer(s) can work with you
to add them: ask in the PR description or comments.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Tests
-----
Your PR should include tests.
For new user facing features in `matrix-react-sdk` or `element-web`, you
must include:
1. Comprehensive unit tests written in Jest. These are located in `/test`.
2. "happy path" end-to-end tests.
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
are run using `element-web`. Ideally, you would also include tests for edge
and error cases.
Unit tests are expected even when the feature is in labs. It's good practice
to write tests alongside the code as it ensures the code is testable from
the start, and gives you a fast feedback loop while you're developing the
functionality. End-to-end tests should be added prior to the feature
leaving labs, but don't have to be present from the start (although it might
be beneficial to have some running early, so you can test things faster).
For bugs in those repos, your change must include at least one unit test or
end-to-end test; which is best depends on what sort of test most concisely
exercises the area.
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
These are located in `/spec/`.
When writing unit tests, please aim for a high level of test coverage
for new code - 80% or greater. If you cannot achieve that, please document
why it's not possible in your PR.
Tests validate that your change works as intended and also document
concisely what is being changed. Ideally, your new tests fail
prior to your change, and succeed once it has been applied. You may
find this simpler to achieve if you write the tests first.
If you're spiking some code that's experimental and not being used to support
production features, exceptions can be made to requirements for tests.
Note that tests will still be required in order to ship the feature, and it's
strongly encouraged to think about tests early in the process, as adding
tests later will become progressively more difficult.
If you're not sure how to approach writing tests for your change, ask for help
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
Code style
----------
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and ***never*** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
-----------
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
--------
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```
Merge Strategy
==============
The preferred method for merging pull requests is squash merging to keep the
commit history trim, but it is up to the discretion of the team member merging
the change. When stacking pull requests, you may wish to do the following:
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
+10 -4
View File
@@ -1,3 +1,11 @@
[![npm](https://img.shields.io/npm/v/matrix-js-sdk)](https://www.npmjs.com/package/matrix-js-sdk)
![Tests](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/tests.yml/badge.svg)
![Static Analysis](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/static_analysis.yml/badge.svg)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=coverage)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
Matrix Javascript SDK
=====================
@@ -25,10 +33,8 @@ In Node.js
----------
Ensure you have the latest LTS version of Node.js installed.
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
a bundler like webpack you'll likely have to transpile dependencies, including this
SDK, to match your target browsers.
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
+22 -18
View File
@@ -1,9 +1,9 @@
{
"name": "matrix-js-sdk",
"version": "17.2.0",
"version": "21.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=12.9.0"
"node": ">=16.0.0"
},
"scripts": {
"prepublishOnly": "yarn build",
@@ -55,14 +55,12 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.7",
"p-retry": "^4.5.0",
"p-retry": "4",
"qs": "^6.9.6",
"request": "^2.88.2",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
@@ -78,31 +76,34 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/jest": "^26.0.20",
"@types/node": "12",
"@types/request": "^2.48.5",
"@types/domexception": "^4.0.0",
"@types/jest": "^29.0.0",
"@types/node": "16",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"babel-jest": "^26.6.3",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "8.9.0",
"domexception": "^4.0.0",
"eslint": "8.24.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.4.0",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.1.2",
"jest": "^26.6.3",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-matrix-org": "^0.6.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"jest": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^27.5.1",
"jest-sonar-reporter": "^2.0.0",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^1.2.3",
"matrix-mock-request": "^2.5.0",
"rimraf": "^3.0.2",
"terser": "^5.5.1",
"tsify": "^5.0.2",
@@ -113,6 +114,9 @@
"testMatch": [
"<rootDir>/spec/**/*.spec.{js,ts}"
],
"setupFilesAfterEnv": [
"<rootDir>/spec/setupTests.ts"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts}"
],
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
#
# Script to perform a post-release steps of matrix-js-sdk.
#
# Requires:
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
for i in main typings
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
git push origin develop
fi
+82 -116
View File
@@ -3,19 +3,16 @@
# Script to perform a release of matrix-js-sdk and downstream projects.
#
# Requires:
# github-changelog-generator; install via:
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
#
# Note: this script is also used to release matrix-react-sdk and element-web.
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
@@ -26,10 +23,9 @@ else
echo "hub is required: please install it"
exit
fi
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
help() {
cat <<EOF
@@ -37,18 +33,9 @@ $USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
-z: skip generating the jsdoc
-n: skip publish to NPM
EOF
}
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
if ! git diff-index --quiet --cached HEAD; then
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
exit
@@ -60,11 +47,8 @@ if ! git diff-files --quiet; then
fi
skip_changelog=
skip_jsdoc=
skip_npm=
changelog_file="CHANGELOG.md"
expected_npm_user="matrixdotorg"
while getopts hc:u:xzn f; do
while getopts hc:x f; do
case $f in
h)
help
@@ -76,24 +60,70 @@ while getopts hc:u:xzn f; do
x)
skip_changelog=1
;;
z)
skip_jsdoc=1
;;
n)
skip_npm=1
;;
u)
expected_npm_user="$OPTARG"
;;
esac
done
shift `expr $OPTIND - 1`
shift $(expr $OPTIND - 1)
if [ $# -ne 1 ]; then
echo "Usage: $USAGE" >&2
exit 1
fi
function check_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Checking version of $1..."
local latestver=$(yarn info -s "$1" dist-tags.next)
if [ "$depver" != "$latestver" ]
then
echo "The latest version of $1 is $latestver but package.json depends on $depver."
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
read resp
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
then
echo "Aborting."
exit 1
fi
if [ "$resp" == "u" ]
then
echo "Upgrading $1 to $latestver..."
yarn add -E "$1@$latestver"
git add -u
git commit -m "Upgrade $1 to $latestver"
fi
fi
}
function reset_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Resetting $1 to develop branch..."
yarn add "github:matrix-org/$1#develop"
git add -u
git commit -m "Reset $1 back to develop branch"
}
has_subprojects=0
if [ -f release_config.yaml ]; then
subprojects=$(cat release_config.yaml | python -c "import yaml; import sys; print(' '.join(list(yaml.load(sys.stdin)['subprojects'].keys())))" 2> /dev/null)
if [ "$?" -eq 0 ]; then
has_subprojects=1
echo "Checking subprojects for upgrades"
for proj in $subprojects; do
check_dependency "$proj"
done
fi
fi
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
@@ -102,20 +132,9 @@ yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts --pure-lockfile
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
if [ -z "$skip_npm" ]; then
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
fi
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
prerelease=0
# We check if this build is a prerelease by looking to
@@ -126,20 +145,11 @@ echo $release | grep -q '-' && prerelease=1
if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
else
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
fi
# we might already be on the release branch, in which case, yay
# If we're on any branch starting with 'release', we don't create
# a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
rel_branch=$(git symbolic-ref --short HEAD)
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
@@ -151,8 +161,8 @@ if [ -z "$skip_changelog" ]; then
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
latest_changes=`mktemp`
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
latest_changes=$(mktemp)
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
set -x
@@ -179,7 +189,7 @@ do
done
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
then
pkglock='yarn.lock'
else
@@ -191,7 +201,7 @@ git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
result=$(cat release_config.yaml | python -c "import yaml; import sys; print(yaml.load(sys.stdin)['signing_id'])" 2> /dev/null || true)
if [ "$?" -eq 0 ]; then
signing_id=$result
fi
@@ -209,8 +219,8 @@ assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=`pwd`
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
projdir=$(pwd)
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
@@ -235,7 +245,7 @@ fi
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else
git tag -a -F "${latest_changes}" "$tag"
fi
@@ -301,7 +311,7 @@ if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
release_text=`mktemp`
release_text=$(mktemp)
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
@@ -313,35 +323,6 @@ fi
rm "${release_text}"
rm "${latest_changes}"
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
# Tag both releases and prereleases as `next` so the last stable release remains
# the default.
if [ -z "$skip_npm" ]; then
npm publish --tag next
if [ $prerelease -eq 0 ]; then
# For a release, also add the default `latest` tag.
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
fi
fi
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
yarn gendoc
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
git pull
cp -a ".jsdoc/matrix-js-sdk/$release" .
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
$release index.html
git add "$release"
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
git push origin gh-pages
fi
# if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then
git checkout "$rel_branch"
@@ -358,34 +339,19 @@ git merge "$rel_branch" --no-edit
git push origin master
# finally, merge master back onto develop (if it exists)
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
git checkout develop
git pull
git merge master --no-edit
# When merging to develop, we need revert the `main` and `typings` fields if
# we adjusted them previously.
for i in main typings
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
git push origin develop
fi
[ -x ./post-release.sh ] && ./post-release.sh
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
echo "Resetting subprojects to develop"
for proj in $subprojects; do
reset_dependency "$proj"
done
git push origin develop
fi
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env node
const fsProm = require('fs/promises');
const PKGJSON = 'package.json';
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
for (const field of ['main', 'typings']) {
if (pkgJson["matrix_lib_"+field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_"+field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
+3 -8
View File
@@ -1,13 +1,6 @@
sonar.projectKey=matrix-js-sdk
sonar.organization=matrix-org
# This is the name and version displayed in the SonarCloud UI.
#sonar.projectName=matrix-js-sdk
#sonar.projectVersion=1.0
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
#sonar.sources=.
# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8
@@ -17,5 +10,7 @@ sonar.exclusions=docs,examples,git-hooks
sonar.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=spec/*.ts
sonar.coverage.exclusions=spec/**/*
sonar.testExecutionReportPaths=coverage/test-report.xml
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
@@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,31 +17,32 @@ limitations under the License.
/**
* A mock implementation of the webstorage api
* @constructor
*/
export function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
}
export class MockStorageApi {
public data: Record<string, string> = {};
public keys: string[] = [];
public length = 0;
MockStorageApi.prototype = {
setItem: function(k, v) {
public setItem(k: string, v: string): void {
this.data[k] = v;
this._recalc();
},
getItem: function(k) {
this.recalc();
}
public getItem(k: string): string | null {
return this.data[k] || null;
},
removeItem: function(k) {
}
public removeItem(k: string): void {
delete this.data[k];
this._recalc();
},
key: function(index) {
this.recalc();
}
public key(index: number): string {
return this.keys[index];
},
_recalc: function() {
const keys = [];
}
private recalc(): void {
const keys: string[] = [];
for (const k in this.data) {
if (!this.data.hasOwnProperty(k)) {
continue;
@@ -50,6 +51,5 @@ MockStorageApi.prototype = {
}
this.keys = keys;
this.length = keys.length;
},
};
}
}
-238
View File
@@ -1,238 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// load olm before the sdk if possible
import './olm-loader';
import MockHttpBackend from 'matrix-mock-request';
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
import { logger } from '../src/logger';
import { WebStorageSessionStore } from "../src/store/session/webstorage";
import { syncPromise } from "./test-utils/test-utils";
import { createClient } from "../src/matrix";
import { MockStorageApi } from "./MockStorageApi";
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @constructor
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
* session store. If undefined, we will create a MockStorageApi.
* @param {object} options additional options to pass to the client
*/
export function TestClient(
userId, deviceId, accessToken, sessionStoreBackend, options,
) {
this.userId = userId;
this.deviceId = deviceId;
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new MockStorageApi();
}
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
options = Object.assign({
baseUrl: "http://" + userId + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: sessionStore,
request: this.httpBackend.requestFn,
}, options);
if (!options.cryptoStore) {
// expose this so the tests can get to it
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
options.cryptoStore = this.cryptoStore;
}
this.client = createClient(options);
this.deviceKeys = null;
this.oneTimeKeys = {};
this.callEventHandler = {
calls: new Map(),
};
}
TestClient.prototype.toString = function() {
return 'TestClient[' + this.userId + ']';
};
/**
* start the client, and wait for it to initialise.
*
* @return {Promise}
*/
TestClient.prototype.start = function() {
logger.log(this + ': starting');
this.httpBackend.when("GET", "/versions").respond(200, {});
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
this.client.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: 'detached',
});
return Promise.all([
this.httpBackend.flushAllExpected(),
syncPromise(this.client),
]).then(() => {
logger.log(this + ': started');
});
};
/**
* stop the client
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
return this.httpBackend.stop();
};
/**
* Set up expectations that the client will upload device keys.
*/
TestClient.prototype.expectDeviceKeyUpload = function() {
const self = this;
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
logger.log(self + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
self.deviceKeys = content.device_keys;
return { one_time_key_counts: { signed_curve25519: 0 } };
});
};
/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns {Promise} for the one-time keys
*/
TestClient.prototype.awaitOneTimeKeyUpload = function() {
if (Object.keys(this.oneTimeKeys).length != 0) {
// already got one-time keys
return Promise.resolve(this.oneTimeKeys);
}
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined);
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
} };
});
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
logger.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
this.oneTimeKeys = content.one_time_keys;
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
} };
});
// this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys;
});
};
/**
* Set up expectations that the client will query device keys.
*
* We check that the query contains each of the users in `response`.
*
* @param {Object} response response to the query.
*/
TestClient.prototype.expectKeyQuery = function(response) {
this.httpBackend.when('POST', '/keys/query').respond(
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual(
[],
"Expected key query for " + userId + ", got " +
Object.keys(content.device_keys),
);
});
return response;
});
};
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getDeviceKey = function() {
const keyId = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getSigningKey = function() {
const keyId = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
/**
* flush a single /sync request, and wait for the syncing event
*
* @returns {Promise} promise which completes once the sync has been flushed
*/
TestClient.prototype.flushSync = function() {
logger.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
syncPromise(this.client),
]).then(() => {
logger.log(`${this}: flushSync completed`);
});
};
TestClient.prototype.isFallbackICEServerAllowed = function() {
return true;
};
+242
View File
@@ -0,0 +1,242 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// load olm before the sdk if possible
import './olm-loader';
import MockHttpBackend from 'matrix-mock-request';
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
import { logger } from '../src/logger';
import { syncPromise } from "./test-utils/test-utils";
import { createClient } from "../src/matrix";
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils";
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
import { IKeyBackupSession } from "../src/crypto/keybackup";
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*/
export class TestClient {
public readonly httpBackend: MockHttpBackend;
public readonly client: MatrixClient;
public deviceKeys: IDeviceKeys;
public oneTimeKeys: Record<string, IOneTimeKey>;
constructor(
public readonly userId?: string,
public readonly deviceId?: string,
accessToken?: string,
sessionStoreBackend?: Storage,
options?: Partial<ICreateClientOpts>,
) {
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new MockStorageApi() as unknown as Storage;
}
this.httpBackend = new MockHttpBackend();
const fullOptions: ICreateClientOpts = {
baseUrl: "http://" + userId?.slice(1).replace(":", ".") + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
...options,
};
if (!fullOptions.cryptoStore) {
// expose this so the tests can get to it
fullOptions.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
}
this.client = createClient(fullOptions);
this.deviceKeys = null;
this.oneTimeKeys = {};
}
public toString(): string {
return 'TestClient[' + this.userId + ']';
}
/**
* start the client, and wait for it to initialise.
*/
public start(): Promise<void> {
logger.log(this + ': starting');
this.httpBackend.when("GET", "/versions").respond(200, {});
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
this.client.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: PendingEventOrdering.Detached,
});
return Promise.all([
this.httpBackend.flushAllExpected(),
syncPromise(this.client),
]).then(() => {
logger.log(this + ': started');
});
}
/**
* stop the client
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
*/
public async stop(): Promise<void> {
this.client.stopClient();
await this.httpBackend.stop();
}
/**
* Set up expectations that the client will upload device keys.
*/
public expectDeviceKeyUpload() {
this.httpBackend.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
logger.log(this + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(this.oneTimeKeys).length).toEqual(0);
this.deviceKeys = content.device_keys;
return { one_time_key_counts: { signed_curve25519: 0 } };
});
}
/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns {Promise} for the one-time keys
*/
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
if (Object.keys(this.oneTimeKeys).length != 0) {
// already got one-time keys
return Promise.resolve(this.oneTimeKeys);
}
this.httpBackend.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined);
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
} };
});
this.httpBackend.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
logger.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
this.oneTimeKeys = content.one_time_keys;
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
} };
});
// this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys;
});
}
/**
* Set up expectations that the client will query device keys.
*
* We check that the query contains each of the users in `response`.
*
* @param {Object} response response to the query.
*/
public expectKeyQuery(response: IDownloadKeyResult) {
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
200, (_path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual([]);
});
return response;
});
}
/**
* Set up expectations that the client will query key backups for a particular session
*/
public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) {
this.httpBackend.when('GET', encodeUri("/room_keys/keys/$roomId/$sessionId", {
$roomId: roomId,
$sessionId: sessionId,
})).respond(status, response);
}
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
*/
public getDeviceKey(): string {
const keyId = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
}
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
*/
public getSigningKey(): string {
const keyId = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
}
/**
* flush a single /sync request, and wait for the syncing event
*/
public flushSync(): Promise<void> {
logger.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
syncPromise(this.client),
]).then(() => {
logger.log(`${this}: flushSync completed`);
});
}
public isFallbackICEServerAllowed(): boolean {
return true;
}
public getUserId(): string {
return this.userId;
}
}
@@ -15,9 +15,11 @@ limitations under the License.
*/
// stub for browser-matrix browserify tests
// @ts-ignore
global.XMLHttpRequest = jest.fn();
afterAll(() => {
// clean up XMLHttpRequest mock
// @ts-ignore
global.XMLHttpRequest = undefined;
});
@@ -14,46 +14,66 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// load XmlHttpRequest mock
import HttpBackend from "matrix-mock-request";
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import type { MatrixClient, ClientEvent } from "../../src";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
matrixcs: {
MatrixClient: typeof MatrixClient;
ClientEvent: typeof ClientEvent;
};
}
}
}
describe("Browserify Test", function() {
let client;
let httpBackend;
let client: MatrixClient;
let httpBackend: HttpBackend;
beforeEach(() => {
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend = new HttpBackend();
client = new global.matrixcs.MatrixClient({
baseUrl: "http://test.server",
userId: USER_ID,
accessToken: ACCESS_TOKEN,
deviceId: DEVICE_ID,
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
client.startClient();
});
afterEach(async () => {
client.stopClient();
httpBackend.stop();
client.http.abort();
httpBackend.verifyNoOutstandingRequests();
httpBackend.verifyNoOutstandingExpectation();
await httpBackend.stop();
});
it("Sync", async function() {
const event = utils.mkMembership({
room: ROOM_ID,
mship: "join",
user: "@other_user:server.test",
name: "Displayname",
});
it("Sync", async () => {
const event = {
type: "m.room.member",
room_id: ROOM_ID,
content: {
membership: "join",
name: "Displayname",
},
event_id: "$foobar",
};
const syncData = {
next_batch: "batch1",
@@ -71,11 +91,16 @@ describe("Browserify Test", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
return await Promise.race([
httpBackend.flushAllExpected(),
new Promise((_, reject) => {
client.once("sync.unexpectedError", reject);
}),
]);
httpBackend.when("GET", "/sync").respond(200, syncData);
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
const unexpectedErrorFn = jest.fn();
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
client.startClient();
await httpBackend.flushAllExpected();
await syncPromise;
expect(unexpectedErrorFn).not.toHaveBeenCalled();
}, 20000); // additional timeout as this test can take quite a while
});
@@ -122,7 +122,7 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
event_id: '$event_id',
});
return Promise.all([
@@ -136,138 +136,137 @@ describe("DeviceList management:", function() {
});
});
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/element-web/issues/3126
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
return aliceTestClient.flushSync();
}).then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
it.skip("We should not get confused by out-of-order device query responses", () => {
// https://github.com/vector-im/element-web/issues/3126
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
return aliceTestClient.flushSync();
}).then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
'@chris:abc': {},
},
},
);
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
'@chris:abc': {},
},
},
);
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
200, { event_id: '$event1' });
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
200, { event_id: '$event1' });
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
]);
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
expect(data.syncToken).toEqual(1);
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
]);
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
expect(data.syncToken).toEqual(1);
});
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '2',
device_lists: {
changed: ['@bob:xyz'],
},
});
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '3',
device_lists: {
changed: ['@chris:abc'],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@chris:abc': {},
},
token: '3',
}).respond(200, {
device_keys: { '@chris:abc': {} },
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
);
}
});
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '2',
device_lists: {
changed: ['@bob:xyz'],
},
});
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '3',
device_lists: {
changed: ['@chris:abc'],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@chris:abc': {},
},
token: '3',
}).respond(200, {
device_keys: { '@chris:abc': {} },
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
);
}
});
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@bob:xyz': {},
},
token: '2',
}).respond(200, {
device_keys: { '@bob:xyz': {} },
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@bob:xyz': {},
},
token: '2',
}).respond(200, {
device_keys: { '@bob:xyz': {} },
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
);
}
});
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
);
}
});
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
const chrisStat = data.trackingStatus['@bob:xyz'];
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
const chrisStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data.syncToken).toEqual(3);
});
});
}).timeout(3000);
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data.syncToken).toEqual(3);
});
});
});
// https://github.com/vector-im/element-web/issues/4983
describe("Alice should know she has stale device lists", () => {
@@ -288,11 +287,12 @@ describe("DeviceList management:", function() {
await aliceTestClient.httpBackend.flush('/keys/query', 1);
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list",
0,
);
});
});
@@ -324,11 +324,12 @@ describe("DeviceList management:", function() {
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
0,
);
});
});
@@ -360,11 +361,12 @@ describe("DeviceList management:", function() {
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
0,
);
});
});
@@ -379,13 +381,15 @@ describe("DeviceList management:", function() {
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
await anotherTestClient.flushSync();
await anotherTestClient.client.crypto.deviceList.saveIfDirty();
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing private property
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
0,
);
});
} finally {
-758
View File
@@ -1,758 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* This file consists of a set of integration tests which try to simulate
* communication via an Olm-encrypted room between two users, Alice and Bob.
*
* Note that megolm (group) conversation is not tested here.
*
* See also `megolm.spec.js`.
*/
// load olm before the sdk if possible
import '../olm-loader';
import { logger } from '../../src/logger';
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED } from "../../src/client";
let aliTestClient;
const roomId = "!room:localhost";
const aliUserId = "@ali:localhost";
const aliDeviceId = "zxcvb";
const aliAccessToken = "aseukfgwef";
let bobTestClient;
const bobUserId = "@bob:localhost";
const bobDeviceId = "bvcxz";
const bobAccessToken = "fewgfkuesa";
let aliMessages;
let bobMessages;
function bobUploadsDeviceKeys() {
bobTestClient.expectDeviceKeyUpload();
return Promise.all([
bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flush(),
]).then(() => {
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
});
}
/**
* Set an expectation that ali will query bobs keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliQueryKeys() {
// can't query keys before bob has uploaded them
expect(bobTestClient.deviceKeys).toBeTruthy();
const bobKeys = {};
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
aliTestClient.httpBackend.when("POST", "/keys/query")
.respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual(
[],
"Expected Alice to key query for " + bobUserId + ", got " +
Object.keys(content.device_keys),
);
const result = {};
result[bobUserId] = bobKeys;
return { device_keys: result };
});
return aliTestClient.httpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that bob will query alis keys; then flush the http request.
*
* @return {promise} which resolves once the http request has completed.
*/
function expectBobQueryKeys() {
// can't query keys before ali has uploaded them
expect(aliTestClient.deviceKeys).toBeTruthy();
const aliKeys = {};
aliKeys[aliDeviceId] = aliTestClient.deviceKeys;
logger.log("query result will be", aliKeys);
bobTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual(
[],
"Expected Bob to key query for " + aliUserId + ", got " +
Object.keys(content.device_keys),
);
const result = {};
result[aliUserId] = aliKeys;
return { device_keys: result };
});
return bobTestClient.httpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliClaimKeys() {
return bobTestClient.awaitOneTimeKeyUpload().then((keys) => {
aliTestClient.httpBackend.when(
"POST", "/keys/claim",
).respond(200, function(path, content) {
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = null;
for (keyId in keys) {
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
break;
}
}
}
const result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
return { one_time_keys: result };
});
}).then(() => {
// it can take a while to process the key query, so give it some extra
// time, and make sure the claim actually happens rather than ploughing on
// confusingly.
return aliTestClient.httpBackend.flush("/keys/claim", 1, 500).then((r) => {
expect(r).toEqual(1, "Ali did not claim Bob's keys");
});
});
}
function aliDownloadsKeys() {
// can't query keys before bob has uploaded them
expect(bobTestClient.getSigningKey()).toBeTruthy();
const p1 = aliTestClient.client.downloadKeys([bobUserId]).then(function() {
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
}).then((devices) => {
expect(devices.length).toEqual(1);
expect(devices[0].deviceId).toEqual("bvcxz");
});
const p2 = expectAliQueryKeys();
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
return Promise.all([p1, p2]).then(() => {
return aliTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data.devices[bobUserId];
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
});
});
}
function aliEnablesEncryption() {
return aliTestClient.client.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(aliTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
});
}
function bobEnablesEncryption() {
return bobTestClient.client.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(bobTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
});
}
/**
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsFirstMessage() {
return Promise.all([
sendMessage(aliTestClient.client),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest),
]).then(function([_, ciphertext]) {
return ciphertext;
});
}
/**
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsMessage() {
return Promise.all([
sendMessage(aliTestClient.client),
expectAliSendMessageRequest(),
]).then(function([_, ciphertext]) {
return ciphertext;
});
}
/**
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
*/
function bobSendsReplyMessage() {
return Promise.all([
sendMessage(bobTestClient.client),
expectBobQueryKeys()
.then(expectBobSendMessageRequest),
]).then(function([_, ciphertext]) {
return ciphertext;
});
}
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectAliSendMessageRequest() {
return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) {
aliMessages.push(content);
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
expect(ciphertext).toBeTruthy();
return ciphertext;
});
}
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectBobSendMessageRequest() {
return expectSendMessageRequest(bobTestClient.httpBackend).then(function(content) {
bobMessages.push(content);
const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeTruthy();
return ciphertext;
});
}
function sendMessage(client) {
return client.sendMessage(
roomId, { msgtype: "m.text", body: "Hello, World" },
);
}
function expectSendMessageRequest(httpBackend) {
const path = "/send/m.room.encrypted/";
const prom = new Promise((resolve) => {
httpBackend.when("PUT", path).respond(200, function(path, content) {
resolve(content);
return {
event_id: "asdfgh",
};
});
});
// it can take a while to process the key query
return httpBackend.flush(path, 1).then(() => prom);
}
function aliRecvMessage() {
const message = bobMessages.shift();
return recvMessage(
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
);
}
function bobRecvMessage() {
const message = aliMessages.shift();
return recvMessage(
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
);
}
function recvMessage(httpBackend, client, sender, message) {
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise((resolve, reject) => {
const onEvent = function(event) {
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
logger.log(client.credentials.userId + " received event",
event);
client.removeListener("event", onEvent);
resolve(event);
};
client.on("event", onEvent);
});
httpBackend.flush();
return eventPromise.then((event) => {
expect(event.isEncrypted()).toBeTruthy();
// it may still be being decrypted
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toMatchObject({
msgtype: "m.text",
body: "Hello, World",
});
expect(event.isEncrypted()).toBeTruthy();
});
}
/**
* Send an initial sync response to the client (which just includes the member
* list for our test room).
*
* @param {TestClient} testClient
* @returns {Promise} which resolves when the sync has been flushed.
*/
function firstSync(testClient) {
// send a sync response including our test room.
const syncData = {
next_batch: "x",
rooms: {
join: { },
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
},
};
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
return testClient.flushSync();
}
describe("MatrixClient crypto", function() {
if (!CRYPTO_ENABLED) {
return;
}
beforeEach(async function() {
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
await bobTestClient.client.initCrypto();
aliMessages = [];
bobMessages = [];
});
afterEach(function() {
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
});
it("Bob uploads device keys", function() {
return Promise.resolve()
.then(bobUploadsDeviceKeys);
});
it("Ali downloads Bobs device keys", function() {
return Promise.resolve()
.then(bobUploadsDeviceKeys)
.then(aliDownloadsKeys);
});
it("Ali gets keys with an invalid signature", function() {
return Promise.resolve()
.then(bobUploadsDeviceKeys)
.then(function() {
// tamper bob's keys
const bobDeviceKeys = bobTestClient.deviceKeys;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
return Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
expectAliQueryKeys(),
]);
}).then(function() {
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
}).then((devices) => {
// should get an empty list
expect(devices).toEqual([]);
});
});
it("Ali gets keys with an incorrect userId", function() {
const eveUserId = "@eve:localhost";
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bvcxz',
keys: {
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
},
user_id: '@eve:localhost',
signatures: {
'@eve:localhost': {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, function(path, content) {
const result = {};
result[bobUserId] = bobKeys;
return { device_keys: result };
});
return Promise.all([
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]).then(function() {
return Promise.all([
aliTestClient.client.getStoredDevicesForUser(bobUserId),
aliTestClient.client.getStoredDevicesForUser(eveUserId),
]);
}).then(([bobDevices, eveDevices]) => {
// should get an empty list
expect(bobDevices).toEqual([]);
expect(eveDevices).toEqual([]);
});
});
it("Ali gets keys with an incorrect deviceId", function() {
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bad_device',
keys: {
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
},
user_id: '@bob:localhost',
signatures: {
'@bob:localhost': {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, function(path, content) {
const result = {};
result[bobUserId] = bobKeys;
return { device_keys: result };
});
return Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]).then(function() {
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
}).then((devices) => {
// should get an empty list
expect(devices).toEqual([]);
});
});
it("Bob starts his client and uploads device keys and one-time keys", function() {
return Promise.resolve()
.then(() => bobTestClient.start())
.then(() => bobTestClient.awaitOneTimeKeyUpload())
.then((keys) => {
expect(Object.keys(keys).length).toEqual(5);
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
});
});
it("Ali sends a message", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage);
});
it("Bob receives a message", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobRecvMessage);
});
it("Bob receives a message with a bogus sender", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(function() {
const message = aliMessages.shift();
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
};
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise((resolve, reject) => {
const onEvent = function(event) {
logger.log(bobUserId + " received event",
event);
resolve(event);
};
bobTestClient.client.once("event", onEvent);
});
bobTestClient.httpBackend.flush();
return eventPromise;
}).then((event) => {
expect(event.isEncrypted()).toBeTruthy();
// it may still be being decrypted
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
});
});
it("Ali blocks Bob's device", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliDownloadsKeys)
.then(function() {
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
const p1 = sendMessage(aliTestClient.client);
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
.then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
return Promise.all([p1, p2]);
});
});
it("Bob receives two pre-key messages", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobRecvMessage)
.then(aliSendsMessage)
.then(bobRecvMessage);
});
it("Bob replies to the message", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(() => firstSync(bobTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobRecvMessage)
.then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1, "Unexpected cipghertext type.");
}).then(aliRecvMessage);
});
it("Ali does a key query when encryption is enabled", function() {
// enabling encryption in the room should make alice download devices
// for both members.
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => firstSync(aliTestClient))
.then(() => {
const syncData = {
next_batch: '2',
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
},
}),
],
},
};
aliTestClient.httpBackend.when('GET', '/sync').respond(
200, syncData);
return aliTestClient.httpBackend.flush('/sync', 1);
}).then(() => {
aliTestClient.expectKeyQuery({
device_keys: {
[bobUserId]: {},
},
});
return aliTestClient.httpBackend.flushAllExpected();
});
});
it("Upload new oneTimeKeys based on a /sync request - no count-asking", function() {
// Send a response which causes a key upload
const httpBackend = aliTestClient.httpBackend;
const syncDataEmpty = {
next_batch: "a",
device_one_time_keys_count: {
signed_curve25519: 0,
},
};
// enqueue expectations:
// * Sync with empty one_time_keys => upload keys
return Promise.resolve()
.then(() => {
logger.log(aliTestClient + ': starting');
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
aliTestClient.client.startClient({});
return httpBackend.flushAllExpected().then(() => {
logger.log(aliTestClient + ': started');
});
})
.then(() => httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
expect(Object.keys(content.one_time_keys).length)
.toBeGreaterThanOrEqual(1);
logger.log('received %i one-time keys',
Object.keys(content.one_time_keys).length);
// cancel futher calls by telling the client
// we have more than we need
return {
one_time_key_counts: {
signed_curve25519: 70,
},
};
}))
.then(() => httpBackend.flushAllExpected());
});
});
+682
View File
@@ -0,0 +1,682 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* This file consists of a set of integration tests which try to simulate
* communication via an Olm-encrypted room between two users, Alice and Bob.
*
* Note that megolm (group) conversation is not tested here.
*
* See also `megolm.spec.js`.
*/
// load olm before the sdk if possible
import '../olm-loader';
import { logger } from '../../src/logger';
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from '../../src/crypto/deviceinfo';
let aliTestClient: TestClient;
const roomId = "!room:localhost";
const aliUserId = "@ali:localhost";
const aliDeviceId = "zxcvb";
const aliAccessToken = "aseukfgwef";
let bobTestClient: TestClient;
const bobUserId = "@bob:localhost";
const bobDeviceId = "bvcxz";
const bobAccessToken = "fewgfkuesa";
let aliMessages: IContent[];
let bobMessages: IContent[];
// IMessage isn't exported by src/crypto/algorithms/olm.ts
interface OlmPayload {
type: number;
body: string;
}
async function bobUploadsDeviceKeys(): Promise<void> {
bobTestClient.expectDeviceKeyUpload();
await Promise.all([
bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flushAllExpected(),
]);
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
}
/**
* Set an expectation that querier will query uploader's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
// can't query keys before bob has uploaded them
expect(uploader.deviceKeys).toBeTruthy();
const uploaderKeys = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
querier.httpBackend.when("POST", "/keys/query")
.respond(200, function(_path, content: IUploadKeysRequest) {
expect(content.device_keys![uploader.userId!]).toEqual([]);
const result = {};
result[uploader.userId!] = uploaderKeys;
return { device_keys: result };
});
return querier.httpBackend.flush("/keys/query", 1);
}
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
async function expectAliClaimKeys(): Promise<void> {
const keys = await bobTestClient.awaitOneTimeKeyUpload();
aliTestClient.httpBackend.when(
"POST", "/keys/claim",
).respond(200, function(_path, content: IUploadKeysRequest) {
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = '';
for (keyId in keys) {
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
break;
}
}
}
const result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
return { one_time_keys: result };
});
// it can take a while to process the key query, so give it some extra
// time, and make sure the claim actually happens rather than ploughing on
// confusingly.
const r = await aliTestClient.httpBackend.flush("/keys/claim", 1, 500);
expect(r).toEqual(1);
}
async function aliDownloadsKeys(): Promise<void> {
// can't query keys before bob has uploaded them
expect(bobTestClient.getSigningKey()).toBeTruthy();
const p1 = async () => {
await aliTestClient.client.downloadKeys([bobUserId]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
expect(devices.length).toEqual(1);
expect(devices[0].deviceId).toEqual("bvcxz");
};
const p2 = expectAliQueryKeys;
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
await Promise.all([p1(), p2()]);
await aliTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore - protected
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data!.devices[bobUserId]!;
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
});
}
async function clientEnablesEncryption(client: MatrixClient): Promise<void> {
await client.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
});
expect(client.isRoomEncrypted(roomId)).toBeTruthy();
}
const aliEnablesEncryption = () => clientEnablesEncryption(aliTestClient.client);
const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client);
/**
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
async function aliSendsFirstMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(aliTestClient.client),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest),
]);
return ciphertext;
}
/**
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
async function aliSendsMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(aliTestClient.client),
expectAliSendMessageRequest(),
]);
return ciphertext;
}
/**
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
*/
async function bobSendsReplyMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(bobTestClient.client),
expectBobQueryKeys()
.then(expectBobSendMessageRequest),
]);
return ciphertext;
}
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
aliMessages.push(content);
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
expect(ciphertext).toBeTruthy();
return ciphertext;
}
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
bobMessages.push(content);
const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeTruthy();
return ciphertext;
}
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
return client.sendMessage(
roomId, { msgtype: "m.text", body: "Hello, World" },
);
}
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
const path = "/send/m.room.encrypted/";
const prom = new Promise<IContent>((resolve) => {
httpBackend.when("PUT", path).respond(200, function(_path, content) {
resolve(content);
return {
event_id: "asdfgh",
};
});
});
// it can take a while to process the key query
await httpBackend.flush(path, 1);
return prom;
}
function aliRecvMessage(): Promise<void> {
const message = bobMessages.shift()!;
return recvMessage(
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
);
}
function bobRecvMessage(): Promise<void> {
const message = aliMessages.shift()!;
return recvMessage(
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
);
}
async function recvMessage(
httpBackend: TestClient["httpBackend"],
client: MatrixClient,
sender: string,
message: IContent,
): Promise<void> {
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function(event: MatrixEvent) {
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
logger.log(client.credentials.userId + " received event",
event);
client.removeListener(ClientEvent.Event, onEvent);
resolve(event);
};
client.on(ClientEvent.Event, onEvent);
});
await httpBackend.flushAllExpected();
const preDecryptionEvent = await eventPromise;
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
// it may still be being decrypted
const event = await testUtils.awaitDecryption(preDecryptionEvent);
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toMatchObject({
msgtype: "m.text",
body: "Hello, World",
});
expect(event.isEncrypted()).toBeTruthy();
}
/**
* Send an initial sync response to the client (which just includes the member
* list for our test room).
*
* @param {TestClient} testClient
* @returns {Promise} which resolves when the sync has been flushed.
*/
function firstSync(testClient: TestClient): Promise<void> {
// send a sync response including our test room.
const syncData = {
next_batch: "x",
rooms: {
join: { },
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
},
};
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
return testClient.flushSync();
}
describe("MatrixClient crypto", () => {
if (!CRYPTO_ENABLED) {
return;
}
beforeEach(async () => {
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
await bobTestClient.client.initCrypto();
aliMessages = [];
bobMessages = [];
});
afterEach(() => {
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
});
it("Bob uploads device keys", bobUploadsDeviceKeys);
it("Ali downloads Bobs device keys", async () => {
await bobUploadsDeviceKeys();
await aliDownloadsKeys();
});
it("Ali gets keys with an invalid signature", async () => {
await bobUploadsDeviceKeys();
// tamper bob's keys
const bobDeviceKeys = bobTestClient.deviceKeys;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
expectAliQueryKeys(),
]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
// should get an empty list
expect(devices).toEqual([]);
});
it("Ali gets keys with an incorrect userId", async () => {
const eveUserId = "@eve:localhost";
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bvcxz',
keys: {
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
},
user_id: '@eve:localhost',
signatures: {
'@eve:localhost': {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]);
const [bobDevices, eveDevices] = await Promise.all([
aliTestClient.client.getStoredDevicesForUser(bobUserId),
aliTestClient.client.getStoredDevicesForUser(eveUserId),
]);
// should get an empty list
expect(bobDevices).toEqual([]);
expect(eveDevices).toEqual([]);
});
it("Ali gets keys with an incorrect deviceId", async () => {
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bad_device',
keys: {
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
},
user_id: '@bob:localhost',
signatures: {
'@bob:localhost': {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
// should get an empty list
expect(devices).toEqual([]);
});
it("Bob starts his client and uploads device keys and one-time keys", async () => {
await bobTestClient.start();
const keys = await bobTestClient.awaitOneTimeKeyUpload();
expect(Object.keys(keys).length).toEqual(5);
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
});
it("Ali sends a message", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
});
it("Bob receives a message", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
await bobRecvMessage();
});
it("Bob receives a message with a bogus sender", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
const message = aliMessages.shift()!;
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
};
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function(event: MatrixEvent) {
logger.log(bobUserId + " received event", event);
resolve(event);
};
bobTestClient.client.once(ClientEvent.Event, onEvent);
});
await bobTestClient.httpBackend.flushAllExpected();
const preDecryptionEvent = await eventPromise;
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
// it may still be being decrypted
const event = await testUtils.awaitDecryption(preDecryptionEvent);
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
});
it("Ali blocks Bob's device", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliDownloadsKeys();
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
const p1 = sendMessage(aliTestClient.client);
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
.then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
await Promise.all([p1, p2]);
});
it("Bob receives two pre-key messages", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
await bobRecvMessage();
await aliSendsMessage();
await bobRecvMessage();
});
it("Bob replies to the message", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
await firstSync(aliTestClient);
await firstSync(bobTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {},
);
await bobRecvMessage();
await bobEnablesEncryption();
const ciphertext = await bobSendsReplyMessage();
expect(ciphertext.type).toEqual(1);
await aliRecvMessage();
});
it("Ali does a key query when encryption is enabled", async () => {
// enabling encryption in the room should make alice download devices
// for both members.
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await firstSync(aliTestClient);
const syncData = {
next_batch: '2',
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
},
}),
],
},
};
aliTestClient.httpBackend.when('GET', '/sync').respond(
200, syncData);
await aliTestClient.httpBackend.flush('/sync', 1);
aliTestClient.expectKeyQuery({
device_keys: {
[bobUserId]: {},
},
failures: {},
});
await aliTestClient.httpBackend.flushAllExpected();
});
it("Upload new oneTimeKeys based on a /sync request - no count-asking", async () => {
// Send a response which causes a key upload
const httpBackend = aliTestClient.httpBackend;
const syncDataEmpty = {
next_batch: "a",
device_one_time_keys_count: {
signed_curve25519: 0,
},
};
// enqueue expectations:
// * Sync with empty one_time_keys => upload keys
logger.log(aliTestClient + ': starting');
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
await Promise.all([
aliTestClient.client.startClient({}),
httpBackend.flushAllExpected(),
]);
logger.log(aliTestClient + ': started');
httpBackend.when("POST", "/keys/upload")
.respond(200, (_path, content: IUploadKeysRequest) => {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
// cancel futher calls by telling the client
// we have more than we need
return {
one_time_key_counts: {
signed_curve25519: 70,
},
};
});
await httpBackend.flushAllExpected();
});
});
@@ -1,25 +1,59 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import {
ClientEvent,
HttpApiEvent,
IEvent,
MatrixClient,
RoomEvent,
RoomMemberEvent,
RoomStateEvent,
UserEvent,
} from "../../src";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient events", function() {
let client;
let httpBackend;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const setupTests = (): [MatrixClient, HttpBackend] => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
const client = testClient.client;
const httpBackend = testClient.httpBackend;
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
return [client!, httpBackend];
};
beforeEach(function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
[client!, httpBackend] = setupTests();
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
httpBackend?.verifyNoOutstandingExpectation();
client?.stopClient();
return httpBackend?.stop();
});
describe("emissions", function() {
@@ -92,53 +126,49 @@ describe("MatrixClient events", function() {
};
it("should emit events from both the first and subsequent /sync calls",
function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
function() {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let expectedEvents = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
let expectedEvents: Partial<IEvent>[] = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
client.on("event", function(event) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
client!.on(ClientEvent.Event, function(event) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
}
}
}
expect(found).toBe(
true, "Unexpected 'event' emitted: " + event.getType(),
);
});
expect(found).toBe(true);
});
client.startClient();
client!.startClient();
return Promise.all([
return Promise.all([
// wait for two SYNCING events
utils.syncPromise(client).then(() => {
return utils.syncPromise(client);
}),
httpBackend.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(
0, "Failed to see all events from /sync calls",
);
utils.syncPromise(client!).then(() => {
return utils.syncPromise(client!);
}),
httpBackend!.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(0);
});
});
});
it("should emit User events", function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let fired = false;
client.on("User.presence", function(event, user) {
client!.on(UserEvent.Presence, function(event, user) {
fired = true;
expect(user).toBeTruthy();
expect(event).toBeTruthy();
@@ -146,58 +176,52 @@ describe("MatrixClient events", function() {
return;
}
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
SYNC_DATA.presence.events[0].content.presence,
SYNC_DATA.presence.events[0]?.content?.presence,
);
});
client.startClient();
client!.startClient();
httpBackend.flushAllExpected().then(function() {
expect(fired).toBe(true, "User.presence didn't fire.");
httpBackend!.flushAllExpected().then(function() {
expect(fired).toBe(true);
done();
});
});
it("should emit Room events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let roomInvokeCount = 0;
let roomNameInvokeCount = 0;
let timelineFireCount = 0;
client.on("Room", function(room) {
client!.on(ClientEvent.Room, function(room) {
roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar");
});
client.on("Room.timeline", function(event, room) {
client!.on(RoomEvent.Timeline, function(event, room) {
timelineFireCount++;
expect(room.roomId).toEqual("!erufh:bar");
expect(room?.roomId).toEqual("!erufh:bar");
});
client.on("Room.name", function(room) {
client!.on(RoomEvent.Name, function(room) {
roomNameInvokeCount++;
});
client.startClient();
client!.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(roomInvokeCount).toEqual(
1, "Room fired wrong number of times.",
);
expect(roomNameInvokeCount).toEqual(
1, "Room.name fired wrong number of times.",
);
expect(timelineFireCount).toEqual(
3, "Room.timeline fired the wrong number of times",
);
expect(roomInvokeCount).toEqual(1);
expect(roomNameInvokeCount).toEqual(1);
expect(timelineFireCount).toEqual(3);
});
});
it("should emit RoomState events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
const roomStateEventTypes = [
"m.room.member", "m.room.create",
@@ -205,126 +229,106 @@ describe("MatrixClient events", function() {
let eventsInvokeCount = 0;
let membersInvokeCount = 0;
let newMemberInvokeCount = 0;
client.on("RoomState.events", function(event, state) {
client!.on(RoomStateEvent.Events, function(event, state) {
eventsInvokeCount++;
const index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
-1, "Unexpected room state event type: " + event.getType(),
);
expect(index).not.toEqual(-1);
if (index >= 0) {
roomStateEventTypes.splice(index, 1);
}
});
client.on("RoomState.members", function(event, state, member) {
client!.on(RoomStateEvent.Members, function(event, state, member) {
membersInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toEqual("join");
});
client.on("RoomState.newMember", function(event, state, member) {
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
newMemberInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toBeFalsy();
});
client.startClient();
client!.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(membersInvokeCount).toEqual(
1, "RoomState.members fired wrong number of times",
);
expect(newMemberInvokeCount).toEqual(
1, "RoomState.newMember fired wrong number of times",
);
expect(eventsInvokeCount).toEqual(
2, "RoomState.events fired wrong number of times",
);
expect(membersInvokeCount).toEqual(1);
expect(newMemberInvokeCount).toEqual(1);
expect(eventsInvokeCount).toEqual(2);
});
});
it("should emit RoomMember events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let typingInvokeCount = 0;
let powerLevelInvokeCount = 0;
let nameInvokeCount = 0;
let membershipInvokeCount = 0;
client.on("RoomMember.name", function(event, member) {
client!.on(RoomMemberEvent.Name, function(event, member) {
nameInvokeCount++;
});
client.on("RoomMember.typing", function(event, member) {
client!.on(RoomMemberEvent.Typing, function(event, member) {
typingInvokeCount++;
expect(member.typing).toBe(true);
});
client.on("RoomMember.powerLevel", function(event, member) {
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
powerLevelInvokeCount++;
});
client.on("RoomMember.membership", function(event, member) {
client!.on(RoomMemberEvent.Membership, function(event, member) {
membershipInvokeCount++;
expect(member.membership).toEqual("join");
});
client.startClient();
client!.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(typingInvokeCount).toEqual(
1, "RoomMember.typing fired wrong number of times",
);
expect(powerLevelInvokeCount).toEqual(
0, "RoomMember.powerLevel fired wrong number of times",
);
expect(nameInvokeCount).toEqual(
0, "RoomMember.name fired wrong number of times",
);
expect(membershipInvokeCount).toEqual(
1, "RoomMember.membership fired wrong number of times",
);
expect(typingInvokeCount).toEqual(1);
expect(powerLevelInvokeCount).toEqual(0);
expect(nameInvokeCount).toEqual(0);
expect(membershipInvokeCount).toEqual(1);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN' };
httpBackend.when("GET", "/sync").respond(401, error);
httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
client!.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
return httpBackend!.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(1);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
httpBackend.when("GET", "/sync").respond(401, error);
httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
client!.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
return httpBackend!.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(1);
});
});
});
@@ -1,813 +0,0 @@
import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/matrix";
import { logger } from "../../src/logger";
import { TestClient } from "../TestClient";
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
});
const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name",
},
});
const INITIAL_SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello",
}),
],
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob",
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId,
},
}),
],
},
},
},
},
};
const EVENTS = [
utils.mkMessage({
room: roomId, user: userId, msg: "we",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "could",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "be",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "heroes",
}),
];
const THREAD_ROOT = utils.mkMessage({
room: roomId,
user: userId,
msg: "thread root",
});
const THREAD_REPLY = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
rel_type: "io.element.thread",
event_id: THREAD_ROOT.event_id,
},
},
});
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
client.startClient();
// set up a promise which will resolve once the client is initialised
const prom = new Promise((resolve) => {
client.on("sync", function(state) {
logger.log("sync", state);
if (state != "SYNCING") {
return;
}
resolve();
});
});
return Promise.all([
httpBackend.flushAllExpected(),
prom,
]);
}
describe("getEventTimeline support", function() {
let httpBackend;
let client;
beforeEach(function() {
const testClient = new TestClient(userId, "DEVICE", accessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
});
afterEach(function() {
if (client) {
client.stopClient();
}
return httpBackend.stop();
});
it("timeline support must be enabled to work", function() {
return startClient(httpBackend, client).then(function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
});
});
it("timeline support works when enabled", function() {
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ timelineSupport: true },
);
client = testClient.client;
httpBackend = testClient.httpBackend;
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
});
});
it("scrollback should be able to scroll back to before a gappy /sync", function() {
// need a client with timelineSupport disabled to make this work
let room;
return startClient(httpBackend, client).then(function() {
room = client.getRoom(roomId);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
],
prev_batch: "f_1_1",
},
},
},
},
});
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[1],
],
limited: true,
prev_batch: "f_1_2",
},
},
},
},
});
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
]);
}).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(EVENTS[1]);
httpBackend.when("GET", "/messages").respond(200, {
chunk: [EVENTS[0]],
start: "pagin_start",
end: "pagin_end",
});
httpBackend.flush("/messages", 1);
return client.scrollback(room);
}).then(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0].event).toEqual(EVENTS[0]);
expect(room.timeline[1].event).toEqual(EVENTS[1]);
expect(room.oldState.paginationToken).toEqual("pagin_end");
});
});
});
describe("MatrixClient event timelines", function() {
let client = null;
let httpBackend = null;
beforeEach(function() {
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ timelineSupport: true },
);
client = testClient.client;
httpBackend = testClient.httpBackend;
return startClient(httpBackend, client);
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
Thread.setServerSideSupport(false);
});
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1], EVENTS[0]],
event: EVENTS[2],
events_after: [EVENTS[3]],
state: [
ROOM_NAME_EVENT,
USER_MEMBERSHIP_EVENT,
],
end: "end_token",
};
});
return Promise.all([
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (let i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token");
}),
httpBackend.flushAllExpected(),
]);
});
it("should return existing timeline for known events", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
],
prev_batch: "f_1_2",
},
},
},
},
});
return Promise.all([
httpBackend.flush("/sync"),
utils.syncPromise(client),
]).then(function() {
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].sender.name).toEqual(userName);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("f_1_1");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
});
});
it("should update timelines where they overlap a previous /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[3],
],
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[2].event_id))
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1]],
event: EVENTS[2],
events_after: [EVENTS[3]],
end: "end_token",
state: [],
};
});
const prom = new Promise((resolve, reject) => {
client.on("sync", function() {
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
}).then(resolve, reject);
});
});
return Promise.all([
httpBackend.flushAllExpected(),
prom,
]);
});
it("should join timelines where they overlap a previous /context", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[2].event_id))
.respond(200, function() {
return {
start: "start_token2",
events_before: [],
event: EVENTS[2],
events_after: [],
end: "end_token2",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[3].event_id))
.respond(200, function() {
return {
start: "start_token3",
events_before: [],
event: EVENTS[3],
events_after: [],
end: "end_token3",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[1].event_id))
.respond(200, function() {
return {
start: "start_token4",
events_before: [EVENTS[0]],
event: EVENTS[1],
events_after: [EVENTS[2], EVENTS[3]],
end: "end_token4",
state: [],
};
});
let tl0;
let tl3;
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(tl0);
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(tl3);
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token3");
}),
httpBackend.flushAllExpected(),
]);
});
it("should fail gracefully if there is no event field", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
.respond(200, function() {
return {
start: "start_token",
events_before: [],
events_after: [],
end: "end_token",
state: [],
};
});
return Promise.all([
client.getEventTimeline(timelineSet, "event1",
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
}, function(e) {
expect(String(e)).toMatch(/'event'/);
}),
httpBackend.flushAllExpected(),
]);
});
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
client.clientOpts.experimentalThreadSupport = true;
Thread.setServerSideSupport(true);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_REPLY.event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: THREAD_REPLY,
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id))
.respond(200, function() {
return THREAD_ROOT;
});
httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id) + "/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) + "?limit=20")
.respond(200, function() {
return {
original_event: THREAD_ROOT,
chunk: [THREAD_REPLY],
next_batch: "next_batch_token0",
prev_batch: "prev_batch_token0",
};
});
const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id);
await httpBackend.flushAllExpected();
const timeline = await timelinePromise;
expect(timeline.getEvents().find(e => e.getId() === THREAD_ROOT.event_id));
expect(timeline.getEvents().find(e => e.getId() === THREAD_REPLY.event_id));
});
});
describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
const params = req.queryParams;
expect(params.dir).toEqual("b");
expect(params.from).toEqual("start_token0");
expect(params.limit).toEqual("30");
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
end: "start_token1",
};
});
let tl;
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, { backwards: true });
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token1");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token0");
}),
httpBackend.flushAllExpected(),
]);
});
it("should allow you to paginate forwards", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
const params = req.queryParams;
expect(params.dir).toEqual("f");
expect(params.from).toEqual("end_token0");
expect(params.limit).toEqual("20");
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
end: "end_token1",
};
});
let tl;
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
tl, { backwards: false, limit: 20 });
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token1");
}),
httpBackend.flushAllExpected(),
]);
});
});
describe("event timeline for sent events", function() {
const TXN_ID = "txn1";
const event = utils.mkMessage({
room: roomId, user: userId, msg: "a body",
});
event.unsigned = { transaction_id: TXN_ID };
beforeEach(function() {
// set up handlers for both the message send, and the
// /sync
httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID)
.respond(200, {
event_id: event.event_id,
});
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
event,
],
prev_batch: "f_1_1",
},
},
},
},
});
});
it("should work when /send returns before /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
return Promise.all([
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
// now let the sync complete, and check it again
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
}),
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1),
]);
});
it("should work when /send returns after /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
return Promise.all([
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
logger.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
logger.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}),
Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
logger.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
// now let the send complete.
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
}),
]);
});
});
it("should handle gappy syncs after redactions", function() {
// https://github.com/vector-im/vector-web/issues/1389
// a state event, followed by a redaction thereof
const event = utils.mkMembership({
room: roomId, mship: "join", user: otherUserId,
});
const redaction = utils.mkEvent({
type: "m.room.redaction",
room_id: roomId,
sender: otherUserId,
content: {},
});
redaction.redacts = event.event_id;
const syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
event,
redaction,
],
limited: false,
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client),
]).then(function() {
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[1].isRedacted()).toBe(true);
const sync2 = {
next_batch: "batch2",
rooms: {
join: {},
},
};
sync2.rooms.join[roomId] = {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "world",
}),
],
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, sync2);
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client),
]);
}).then(function() {
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
});
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5,11 +5,11 @@ import { MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api";
import { IStore } from "../../src/store";
describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
let httpBackend = new HttpBackend();
const userId = "@alice:localhost";
const userB = "@bob:localhost";
const accessToken = "aseukfgwef";
@@ -65,9 +65,10 @@ describe("MatrixClient opts", function() {
});
describe("without opts.store", function() {
let client;
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: undefined,
baseUrl: baseUrl,
userId: userId,
@@ -99,7 +100,7 @@ describe("MatrixClient opts", function() {
];
client.on("event", function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1, "Recv unexpected event type: " + event.getType(),
-1,
);
expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1,
@@ -118,16 +119,17 @@ describe("MatrixClient opts", function() {
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
0,
);
});
});
describe("without opts.scheduler", function() {
let client;
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
store: new MemoryStore(),
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: new MemoryStore() as IStore,
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -135,13 +137,17 @@ describe("MatrixClient opts", function() {
});
});
afterEach(function() {
client.stopClient();
});
it("shouldn't retry sending events", function(done) {
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({
errcode: "M_SOMETHING",
error: "Ruh roh",
}));
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
expect(false).toBe(true);
}, function(err) {
expect(err.errcode).toEqual("M_SOMETHING");
done();
+127
View File
@@ -0,0 +1,127 @@
/*
Copyright 2022 Dominik Henneke
Copyright 2022 Nordeck IT + Consulting GmbH.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import { Direction, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { TestClient } from "../TestClient";
describe("MatrixClient relations", () => {
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const setupTests = (): [MatrixClient, HttpBackend] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ scheduler },
);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
return [client, httpBackend];
};
beforeEach(() => {
[client, httpBackend] = setupTests();
});
afterEach(() => {
httpBackend!.verifyNoOutstandingExpectation();
return httpBackend!.stop();
});
it("should read related events with the default options", async () => {
const response = client!.relations(roomId, '$event-0', null, null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
});
it("should read related events with relation type", async () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
});
it("should read related events with relation type and event type", async () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
});
it("should read related events with custom options", async () => {
const response = client!.relations(roomId, '$event-0', null, null, {
dir: Direction.Forward,
from: 'FROM',
limit: 10,
to: 'TO',
});
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
});
it('should use default direction in the fetchRelations endpoint', async () => {
const response = client!.fetchRelations(roomId, '$event-0', null, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" });
});
});
+45 -23
View File
@@ -1,19 +1,35 @@
import { EventStatus, RoomEvent } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() {
let client: TestClient = null;
let httpBackend: TestClient["httpBackend"] = null;
let scheduler;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let room: Room;
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
let room: Room | undefined;
beforeEach(function() {
scheduler = new MatrixScheduler();
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
@@ -21,15 +37,21 @@ describe("MatrixClient retrying", function() {
undefined,
{ scheduler },
);
httpBackend = testClient.httpBackend;
client = testClient.client;
room = new Room(roomId, client, userId);
client.store.storeRoom(room);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
const room = new Room(roomId, client, userId);
client!.store.storeRoom(room);
return [client, httpBackend, room];
};
beforeEach(function() {
[client, httpBackend, room] = setupTests();
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
httpBackend!.verifyNoOutstandingExpectation();
return httpBackend!.stop();
});
xit("should retry according to MatrixScheduler.retryFn", function() {
@@ -50,7 +72,7 @@ describe("MatrixClient retrying", function() {
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued
const p1 = client.sendMessage(roomId, {
const p1 = client!.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m1",
}).then(function() {
@@ -63,13 +85,13 @@ describe("MatrixClient retrying", function() {
// XXX: it turns out that the promise returned by this message
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client.sendMessage(roomId, {
client!.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m2",
});
// both events should be in the timeline at this point
const tl = room.getLiveTimeline().getEvents();
const tl = room!.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
const ev1 = tl[0];
const ev2 = tl[1];
@@ -78,24 +100,24 @@ describe("MatrixClient retrying", function() {
expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
client!.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() {
client.cancelPendingEvent(ev1);
client!.cancelPendingEvent(ev1);
}).toThrow();
}).respond(400); // fail the first message
// wait for the localecho of ev1 to be updated
const p3 = new Promise<void>((resolve, reject) => {
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
room!.on(RoomEvent.LocalEchoUpdated, (ev0) => {
if (ev0 === ev1) {
resolve();
}
@@ -105,7 +127,7 @@ describe("MatrixClient retrying", function() {
expect(tl.length).toEqual(1);
// cancel the first message
client.cancelPendingEvent(ev1);
client!.cancelPendingEvent(ev1);
expect(ev1.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(0);
});
@@ -113,7 +135,7 @@ describe("MatrixClient retrying", function() {
return Promise.all([
p1,
p3,
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
});
@@ -1,611 +0,0 @@
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() {
let client = null;
let httpBackend = null;
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
});
const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name",
},
});
let NEXT_SYNC_DATA;
const SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello",
}),
],
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob",
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId,
},
}),
],
},
},
},
},
};
function setNextSyncData(events) {
events = events || [];
NEXT_SYNC_DATA = {
next_batch: "n",
presence: { events: [] },
rooms: {
invite: {},
join: {
"!foo:bar": {
timeline: { events: [] },
state: { events: [] },
ephemeral: { events: [] },
},
},
leave: {},
},
};
events.forEach(function(e) {
if (e.room_id !== roomId) {
throw new Error("setNextSyncData only works with one room id");
}
if (e.state_key) {
if (e.__prev_event === undefined) {
throw new Error(
"setNextSyncData needs the prev state set to '__prev_event' " +
"for " + e.type,
);
}
if (e.__prev_event !== null) {
// push the previous state for this event type
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
}
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
} else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
});
}
beforeEach(async function() {
// these tests should work with or without timelineSupport
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ timelineSupport: true },
);
httpBackend = testClient.httpBackend;
client = testClient.client;
setNextSyncData();
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
client.startClient();
await httpBackend.flush("/versions");
await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter");
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.sendTextMessage(roomId, "I am a fish", "txn1");
// check it was added
expect(room.timeline.length).toEqual(2);
// check status
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
// check member
const member = room.timeline[1].sender;
expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName);
httpBackend.flush("/sync", 1).then(function() {
done();
});
});
httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"BEFORE the event comes down the event stream", function(done) {
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
client.sendTextMessage(roomId, "I am a fish", "txn1").then(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend.flush("/sync", 1).then(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
httpBackend.flush("/txn1", 1);
});
httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream", function(done) {
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/sync", 1).then(function() {
expect(room.timeline.length).toEqual(2);
httpBackend.flush("/txn1", 1);
promise.then(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
});
httpBackend.flush("/sync", 1);
});
});
describe("paginated events", function() {
let sbEvents;
const sbEndTok = "pagin_end";
beforeEach(function() {
sbEvents = [];
httpBackend.when("GET", "/messages").respond(200, function() {
return {
chunk: sbEvents,
start: "pagin_start",
end: sbEndTok,
};
});
});
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBe(null);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
});
httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
// We're aiming for an eventual timeline of:
//
// 'Old Alice' joined the room
// <Old Alice> I'm old alice
// @alice:localhost changed their name from 'Old Alice' to 'Alice'
// <Alice> I'm alice
// ------^ /messages results above this point, /sync result below
// <Bob> hello
// make an m.room.member event for alice's join
const joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice",
url: null,
});
// make an m.room.member event with prev_content for alice's nick
// change
const oldMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: userName,
url: "mxc://some/url",
});
oldMshipEvent.prev_content = {
displayname: "Old Alice",
avatar_url: null,
membership: "join",
};
// set the list of events to return on scrollback (/messages)
// N.B. synapse returns /messages in reverse chronological order
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I'm alice",
}),
oldMshipEvent,
utils.mkMessage({
user: userId, room: roomId, msg: "I'm old alice",
}),
joinMshipEvent,
];
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
// sync response
expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(5);
const joinMsg = room.timeline[0];
expect(joinMsg.sender.name).toEqual("Old Alice");
const oldMsg = room.timeline[1];
expect(oldMsg.sender.name).toEqual("Old Alice");
const newMsg = room.timeline[3];
expect(newMsg.sender.name).toEqual(userName);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
});
httpBackend.flush("/sync", 1);
});
it("should add it them to the right place in the timeline", function(done) {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new",
}),
utils.mkMessage({
user: userId, room: roomId, msg: "I am old",
}),
];
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
});
httpBackend.flush("/sync", 1);
});
it("should use 'end' as the next pagination token", function(done) {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new",
}),
];
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeTruthy();
client.scrollback(room, 1).then(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend.flush("/messages", 1).then(function() {
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
});
httpBackend.flush("/sync", 1);
});
});
describe("new events", function() {
it("should be added to the right place in the timeline", function() {
const eventData = [
utils.mkMessage({ user: userId, room: roomId }),
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
let index = 0;
client.on("Room.timeline", function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
expect(event.event).toEqual(eventData[index]);
index += 1;
});
httpBackend.flush("/messages", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(index).toEqual(2);
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
eventData[1],
);
expect(room.timeline[1].event).toEqual(
eventData[0],
);
});
});
});
it("should set the right event.sender values", function() {
const eventData = [
utils.mkMessage({ user: userId, room: roomId }),
utils.mkMembership({
user: userId, room: roomId, mship: "join", name: "New Name",
}),
utils.mkMessage({ user: userId, room: roomId }),
];
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
});
});
});
it("should set the right room.name", function() {
const secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2",
},
});
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
let nameEmitCount = 0;
client.on("Room.name", function(rm) {
nameEmitCount += 1;
});
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
const thirdRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3",
},
});
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).then(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
});
});
});
it("should set the right room members", function() {
const userC = "@cee:bar";
const userD = "@dee:bar";
const eventData = [
utils.mkMembership({
user: userC, room: roomId, mship: "join", name: "C",
}),
utils.mkMembership({
user: userC, room: roomId, mship: "invite", skey: userD,
}),
];
eventData[0].__prev_event = null;
eventData[1].__prev_event = null;
setNextSyncData(eventData);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
"join",
);
expect(room.currentState.getMember(userD).name).toEqual(userD);
expect(room.currentState.getMember(userD).membership).toEqual(
"invite",
);
});
});
});
});
describe("gappy sync", function() {
it("should copy the last known state to the new timeline", function() {
const eventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend.flush("/versions", 1),
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
httpBackend.flush("/messages", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId).name).toEqual(userName);
expect(room.currentState.getMember(userId).membership).toEqual(
"join",
);
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId).membership).toEqual(
"join",
);
});
});
});
it("should emit a 'Room.timelineReset' event", function() {
const eventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
let emitCount = 0;
client.on("Room.timelineReset", function(emitRoom) {
expect(emitRoom).toEqual(room);
emitCount++;
});
httpBackend.flush("/messages", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(emitCount).toEqual(1);
});
});
});
});
});
@@ -0,0 +1,884 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() {
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
});
const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name",
},
});
let NEXT_SYNC_DATA;
const SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello",
}),
],
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob",
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId,
},
}),
],
},
},
},
},
};
function setNextSyncData(events: Partial<IEvent>[] = []) {
NEXT_SYNC_DATA = {
next_batch: "n",
presence: { events: [] },
rooms: {
invite: {},
join: {
"!foo:bar": {
timeline: { events: [] },
state: { events: [] },
ephemeral: { events: [] },
},
},
leave: {},
},
};
events.forEach(function(e) {
if (e.room_id !== roomId) {
throw new Error("setNextSyncData only works with one room id");
}
if (e.state_key) {
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
} else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
} else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
});
}
const setupTestClient = (): [MatrixClient, HttpBackend] => {
// these tests should work with or without timelineSupport
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ timelineSupport: true },
);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
setNextSyncData();
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
client!.startClient();
return [client!, httpBackend];
};
beforeEach(async function() {
[client!, httpBackend] = setupTestClient();
await httpBackend.flush("/versions");
await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter");
});
afterEach(function() {
httpBackend!.verifyNoOutstandingExpectation();
client!.stopClient();
return httpBackend!.stop();
});
describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client!.sendTextMessage(roomId, "I am a fish", "txn1");
// check it was added
expect(room.timeline.length).toEqual(2);
// check status
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
// check member
const member = room.timeline[1].sender;
expect(member?.userId).toEqual(userId);
expect(member?.name).toEqual(userName);
httpBackend!.flush("/sync", 1).then(function() {
done();
});
});
httpBackend!.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"BEFORE the event comes down the event stream", function(done) {
const eventId = "$foo:bar";
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
msg: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend!.flush("/sync", 1).then(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
httpBackend!.flush("/txn1", 1);
});
httpBackend!.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream", function(done) {
const eventId = "$foo:bar";
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
msg: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend!.flush("/sync", 1).then(function() {
expect(room.timeline.length).toEqual(2);
httpBackend!.flush("/txn1", 1);
promise.then(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
});
httpBackend!.flush("/sync", 1);
});
});
describe("paginated events", function() {
let sbEvents;
const sbEndTok = "pagin_end";
beforeEach(function() {
sbEvents = [];
httpBackend!.when("GET", "/messages").respond(200, function() {
return {
chunk: sbEvents,
start: "pagin_start",
end: sbEndTok,
};
});
});
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBe(null);
// still have a sync to flush
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
httpBackend!.flush("/messages", 1);
});
httpBackend!.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
// We're aiming for an eventual timeline of:
//
// 'Old Alice' joined the room
// <Old Alice> I'm old alice
// @alice:localhost changed their name from 'Old Alice' to 'Alice'
// <Alice> I'm alice
// ------^ /messages results above this point, /sync result below
// <Bob> hello
// make an m.room.member event for alice's join
const joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice",
url: undefined,
});
// make an m.room.member event with prev_content for alice's nick
// change
const oldMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: userName,
url: "mxc://some/url",
});
oldMshipEvent.prev_content = {
displayname: "Old Alice",
avatar_url: undefined,
membership: "join",
};
// set the list of events to return on scrollback (/messages)
// N.B. synapse returns /messages in reverse chronological order
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I'm alice",
}),
oldMshipEvent,
utils.mkMessage({
user: userId, room: roomId, msg: "I'm old alice",
}),
joinMshipEvent,
];
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
// sync response
expect(room.timeline.length).toEqual(1);
client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(5);
const joinMsg = room.timeline[0];
expect(joinMsg.sender?.name).toEqual("Old Alice");
const oldMsg = room.timeline[1];
expect(oldMsg.sender?.name).toEqual("Old Alice");
const newMsg = room.timeline[3];
expect(newMsg.sender?.name).toEqual(userName);
// still have a sync to flush
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
httpBackend!.flush("/messages", 1);
});
httpBackend!.flush("/sync", 1);
});
it("should add it them to the right place in the timeline", function(done) {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new",
}),
utils.mkMessage({
user: userId, room: roomId, msg: "I am old",
}),
];
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]);
// still have a sync to flush
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
httpBackend!.flush("/messages", 1);
});
httpBackend!.flush("/sync", 1);
});
it("should use 'end' as the next pagination token", function(done) {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new",
}),
];
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.oldState.paginationToken).toBeTruthy();
client!.scrollback(room, 1).then(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend!.flush("/messages", 1).then(function() {
// still have a sync to flush
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
});
httpBackend!.flush("/sync", 1);
});
});
describe("new events", function() {
it("should be added to the right place in the timeline", function() {
const eventData = [
utils.mkMessage({ user: userId, room: roomId }),
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData);
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!;
let index = 0;
client!.on(RoomEvent.Timeline, function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
expect(event.event).toEqual(eventData[index]);
index += 1;
});
httpBackend!.flush("/messages", 1);
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(index).toEqual(2);
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
eventData[1],
);
expect(room.timeline[1].event).toEqual(
eventData[0],
);
});
});
});
it("should set the right event.sender values", function() {
const eventData = [
utils.mkMessage({ user: userId, room: roomId }),
utils.mkMembership({
user: userId, room: roomId, mship: "join", name: "New Name",
}),
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData);
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!;
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender?.name).toEqual(userName);
expect(postNameEvent.sender?.name).toEqual("New Name");
});
});
});
it("should set the right room.name", function() {
const secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2",
},
});
setNextSyncData([secondRoomNameEvent]);
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!;
let nameEmitCount = 0;
client!.on(RoomEvent.Name, function(rm) {
nameEmitCount += 1;
});
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
const thirdRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3",
},
});
setNextSyncData([thirdRoomNameEvent]);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]);
}).then(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
});
});
});
it("should set the right room members", function() {
const userC = "@cee:bar";
const userD = "@dee:bar";
const eventData = [
utils.mkMembership({
user: userC, room: roomId, mship: "join", name: "C",
}),
utils.mkMembership({
user: userC, room: roomId, mship: "invite", skey: userD,
}),
];
setNextSyncData(eventData);
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!;
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC)!.name).toEqual("C");
expect(room.currentState.getMember(userC)!.membership).toEqual(
"join",
);
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
expect(room.currentState.getMember(userD)!.membership).toEqual(
"invite",
);
});
});
});
});
describe("gappy sync", function() {
it("should copy the last known state to the new timeline", function() {
const eventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend!.flush("/versions", 1),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!;
httpBackend!.flush("/messages", 1);
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
expect(room.currentState.getMember(userId)!.membership).toEqual(
"join",
);
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(
"join",
);
});
});
});
it("should emit a `RoomEvent.TimelineReset` event when the sync response is `limited`", function() {
const eventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client!.getRoom(roomId)!;
let emitCount = 0;
client!.on(RoomEvent.TimelineReset, function(emitRoom) {
expect(emitRoom).toEqual(room);
emitCount++;
});
httpBackend!.flush("/messages", 1);
return Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(emitCount).toEqual(1);
});
});
});
});
describe('Refresh live timeline', () => {
const initialSyncEventData = [
utils.mkMessage({ user: userId, room: roomId }),
utils.mkMessage({ user: userId, room: roomId }),
utils.mkMessage({ user: userId, room: roomId }),
];
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
const contextResponse = {
start: "start_token",
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
event: initialSyncEventData[2],
events_after: [],
state: [
USER_MEMBERSHIP_EVENT,
],
end: "end_token",
};
let room;
beforeEach(async () => {
setNextSyncData(initialSyncEventData);
// Create a room from the sync
await Promise.all([
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Get the room after the first sync so the room is created
room = client!.getRoom(roomId)!;
expect(room).toBeTruthy();
});
it('should clear and refresh messages in timeline', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from.
httpBackend!.when("GET", contextUrl)
.respond(200, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
return contextResponse;
});
// Refresh the timeline.
await Promise.all([
room.refreshLiveTimeline(),
httpBackend!.flushAllExpected(),
]);
// Make sure the message are visible
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
expect(resultantEventIdsInTimeline).toEqual([
initialSyncEventData[0].event_id,
initialSyncEventData[1].event_id,
initialSyncEventData[2].event_id,
]);
});
it('Perfectly merges timelines if a sync finishes while refreshing the timeline', async () => {
// `/context` request for `refreshLiveTimeline()` ->
// `getEventTimeline()` to construct a new timeline from.
//
// We only resolve this request after we detect that the timeline
// was reset(when it goes blank) and force a sync to happen in the
// middle of all of this refresh timeline logic. We want to make
// sure the sync pagination still works as expected after messing
// the refresh timline logic messes with the pagination tokens.
httpBackend!.when("GET", contextUrl)
.respond(200, () => {
// Now finally return and make the `/context` request respond
return contextResponse;
});
// Wait for the timeline to reset(when it goes blank) which means
// it's in the middle of the refrsh logic right before the
// `getEventTimeline()` -> `/context`. Then simulate a racey `/sync`
// to happen in the middle of all of this refresh timeline logic. We
// want to make sure the sync pagination still works as expected
// after messing the refresh timline logic messes with the
// pagination tokens.
//
// We define this here so the event listener is in place before we
// call `room.refreshLiveTimeline()`.
const racingSyncEventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => {
let eventFired = false;
// Throw a more descriptive error if this part of the test times out.
const failTimeout = setTimeout(() => {
if (eventFired) {
reject(new Error(
'TestError: `RoomEvent.TimelineReset` fired but we timed out trying to make' +
'a `/sync` happen in time.',
));
} else {
reject(new Error(
'TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire.',
));
}
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
room.on(RoomEvent.TimelineReset, async () => {
try {
eventFired = true;
// The timeline should be cleared at this point in the refresh
expect(room.getUnfilteredTimelineSet().getLiveTimeline().getEvents().length).toEqual(0);
// Then make a `/sync` happen by sending a message and seeing that it
// shows up (simulate a /sync naturally racing with us).
setNextSyncData(racingSyncEventData);
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!, 1),
]);
// Make sure the timeline has the racey sync data
const afterRaceySyncTimelineEvents = room
.getUnfilteredTimelineSet()
.getLiveTimeline()
.getEvents();
const afterRaceySyncTimelineEventIds = afterRaceySyncTimelineEvents
.map((event) => event.getId());
expect(afterRaceySyncTimelineEventIds).toEqual([
racingSyncEventData[0].event_id,
]);
clearTimeout(failTimeout);
resolve();
} catch (err) {
reject(err);
}
});
});
// Refresh the timeline. Just start the function, we will wait for
// it to finish after the racey sync.
const refreshLiveTimelinePromise = room.refreshLiveTimeline();
await waitForRaceySyncAfterResetPromise;
await Promise.all([
refreshLiveTimelinePromise,
// Then flush the remaining `/context` to left the refresh logic complete
httpBackend!.flushAllExpected(),
]);
// Make sure sync pagination still works by seeing a new message show up
// after refreshing the timeline.
const afterRefreshEventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData);
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Make sure the timeline includes the the events from the `/sync`
// that raced and beat us in the middle of everything and the
// `/sync` after the refresh. Since the `/sync` beat us to create
// the timeline, `initialSyncEventData` won't be visible unless we
// paginate backwards with `/messages`.
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
expect(resultantEventIdsInTimeline).toEqual([
racingSyncEventData[0].event_id,
afterRefreshEventData[0].event_id,
]);
});
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from.
httpBackend!.when("GET", contextUrl).check(() => {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
}).respond(500, new MatrixError({
errcode: 'TEST_FAKE_ERROR',
error: 'We purposely intercepted this /context request to make it fail ' +
'in order to test whether the refresh timeline code is resilient',
}));
// Refresh the timeline and expect it to fail
const settledFailedRefreshPromises = await Promise.allSettled([
room.refreshLiveTimeline(),
httpBackend!.flushAllExpected(),
]);
// We only expect `TEST_FAKE_ERROR` here. Anything else is
// unexpected and should fail the test.
if (settledFailedRefreshPromises[0].status === 'fulfilled') {
throw new Error('Expected the /context request to fail with a 500');
} else if (settledFailedRefreshPromises[0].reason.errcode !== 'TEST_FAKE_ERROR') {
throw settledFailedRefreshPromises[0].reason;
}
// The timeline will be empty after we refresh the timeline and fail
// to construct a new timeline.
expect(room.timeline.length).toEqual(0);
// `/messages` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` to construct a new timeline from.
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
.respond(200, function() {
return {
chunk: [{
// The latest message in the room
event_id: initialSyncEventData[2].event_id,
}],
};
});
// `/context` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
// timeline from.
httpBackend!.when("GET", contextUrl)
.respond(200, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
return contextResponse;
});
// Refresh the timeline again but this time it should pass
await Promise.all([
room.refreshLiveTimeline(),
httpBackend!.flushAllExpected(),
]);
// Make sure sync pagination still works by seeing a new message show up
// after refreshing the timeline.
const afterRefreshEventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData);
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Make sure the message are visible
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
expect(resultantEventIdsInTimeline).toEqual([
initialSyncEventData[0].event_id,
initialSyncEventData[1].event_id,
initialSyncEventData[2].event_id,
afterRefreshEventData[0].event_id,
]);
});
});
});
-755
View File
@@ -1,755 +0,0 @@
import { MatrixEvent } from "../../src/models/event";
import { EventTimeline } from "../../src/models/event-timeline";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient syncing", function() {
let client = null;
let httpBackend = null;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const otherUserId = "@bob:localhost";
const userA = "@alice:bar";
const userB = "@bob:bar";
const userC = "@claire:bar";
const roomOne = "!foo:localhost";
const roomTwo = "!bar:localhost";
beforeEach(function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("startClient", function() {
const syncData = {
next_batch: "batch_token",
rooms: {},
presence: {},
};
it("should /sync after /pushrules and /filter.", function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flushAllExpected().then(function() {
done();
});
});
it("should pass the 'next_batch' token from /sync to the since= param " +
" of the next /sync", function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").check(function(req) {
expect(req.queryParams.since).toEqual(syncData.next_batch);
}).respond(200, syncData);
client.startClient();
httpBackend.flushAllExpected().then(function() {
done();
});
});
});
describe("resolving invites to profile info", function() {
const syncData = {
next_batch: "s_5_3",
presence: {
events: [],
},
rooms: {
join: {
},
},
};
beforeEach(function() {
syncData.presence.events = [];
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
});
it("should resolve incoming invites from /sync", function() {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(
200, {
avatar_url: "mxc://flibble/wibble",
displayname: "The Boss",
},
);
client.startClient({
resolveInvitesToProfiles: true,
});
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Boss");
expect(
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBeTruthy();
});
});
it("should use cached values from m.presence wherever possible", function() {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost",
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient({
resolveInvitesToProfiles: true,
});
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Ghost");
});
});
it("should result in events on the room member firing", function() {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost",
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
let latestFiredName = null;
client.on("RoomMember.name", function(event, m) {
if (m.userId === userC && m.roomId === roomOne) {
latestFiredName = m.name;
}
});
client.startClient({
resolveInvitesToProfiles: true,
});
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
expect(latestFiredName).toEqual("The Ghost");
});
});
it("should no-op if resolveInvitesToProfiles is not set", function() {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual(userC);
expect(
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBe(null);
});
});
});
describe("users", function() {
const syncData = {
next_batch: "nb",
presence: {
events: [
utils.mkPresence({
user: userA, presence: "online",
}),
utils.mkPresence({
user: userB, presence: "unavailable",
}),
],
},
};
it("should create users for presence events from /sync",
function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
expect(client.getUser(userA).presence).toEqual("online");
expect(client.getUser(userB).presence).toEqual("unavailable");
});
});
});
describe("room state", function() {
const msgText = "some text here";
const otherDisplayName = "Bob Smith";
const syncData = {
rooms: {
join: {
},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
const nextSyncData = {
rooms: {
join: {
},
},
};
nextSyncData.rooms.join[roomOne] = {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" },
}),
],
},
};
nextSyncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText,
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] },
}),
],
},
};
it("should continually recalculate the right room name.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
expect(room.name).toEqual(
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
);
});
});
it("should store the right events in the timeline.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
// should have added the message from /events
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getContent().body).toEqual(msgText);
});
});
it("should set the right room name.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
// should use the display name of the other person.
expect(room.name).toEqual(otherDisplayName);
});
});
it("should set the right user's typing flag.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
let member = room.getMember(otherUserId);
expect(member).toBeTruthy();
expect(member.typing).toEqual(true);
member = room.getMember(selfUserId);
expect(member).toBeTruthy();
expect(member.typing).toEqual(false);
});
});
// XXX: This test asserts that the js-sdk obeys the spec and treats state
// events that arrive in the incremental sync as if they preceeded the
// timeline events, however this breaks peeking, so it's disabled
// (see sync.js)
xit("should correctly interpret state in incremental sync.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomOne);
const stateAtStart = room.getLiveTimeline().getState(
EventTimeline.BACKWARDS,
);
const startRoomNameEvent = stateAtStart.getStateEvents('m.room.name', '');
expect(startRoomNameEvent.getContent().name).toEqual('Old room name');
const stateAtEnd = room.getLiveTimeline().getState(
EventTimeline.FORWARDS,
);
const endRoomNameEvent = stateAtEnd.getStateEvents('m.room.name', '');
expect(endRoomNameEvent.getContent().name).toEqual('A new room name');
});
});
xit("should update power levels for users in a room", function() {
});
xit("should update the room topic", function() {
});
});
describe("timeline", function() {
beforeEach(function() {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
});
it("should set the back-pagination token on new rooms", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "roomtwo",
}),
],
prev_batch: "roomtwotok",
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomTwo);
expect(room).toBeDefined();
const tok = room.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("roomtwotok");
});
});
it("should set the back-pagination token on gappy syncs", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
let resetCallCount = 0;
// the token should be set *before* timelineReset is emitted
client.on("Room.timelineReset", function(room) {
resetCallCount++;
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(0);
const tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("newerTok");
});
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomOne);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
expect(resetCallCount).toEqual(1);
});
});
});
describe("receipts", function() {
const syncData = {
rooms: {
join: {
},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
beforeEach(function() {
syncData.rooms.join[roomOne].ephemeral = {
events: [],
};
});
it("should sync receipts from /sync.", function() {
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
const receipt = {};
receipt[ackEvent.event_id] = {
"m.read": {},
};
receipt[ackEvent.event_id]["m.read"][userC] = {
ts: 176592842636,
};
syncData.rooms.join[roomOne].ephemeral.events = [{
content: receipt,
room_id: roomOne,
type: "m.receipt",
}];
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomOne);
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
type: "m.read",
userId: userC,
data: {
ts: 176592842636,
},
}]);
});
});
});
describe("of a room", function() {
xit("should sync when a join event (which changes state) for the user" +
" arrives down the event stream (e.g. join from another device)", function() {
});
xit("should sync when the user explicitly calls joinRoom", function() {
});
});
describe("syncLeftRooms", function() {
beforeEach(function(done) {
client.startClient();
httpBackend.flushAllExpected().then(function() {
// the /sync call from syncLeftRooms ends up in the request
// queue behind the call from the running client; add a response
// to flush the client's one out.
httpBackend.when("GET", "/sync").respond(200, {});
done();
});
});
it("should create and use an appropriate filter", function() {
httpBackend.when("POST", "/filter").check(function(req) {
expect(req.data).toEqual({
room: { timeline: { limit: 1 },
include_leave: true } });
}).respond(200, { filter_id: "another_id" });
const prom = new Promise((resolve) => {
httpBackend.when("GET", "/sync").check(function(req) {
expect(req.queryParams.filter).toEqual("another_id");
resolve();
}).respond(200, {});
});
client.syncLeftRooms();
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
return Promise.all([
httpBackend.flush("/filter").then(function() {
// flush the syncs
return httpBackend.flushAllExpected();
}),
prom,
]);
});
it("should set the back-pagination token on left rooms", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
leave: {},
},
};
syncData.rooms.leave[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
},
};
httpBackend.when("POST", "/filter").respond(200, {
filter_id: "another_id",
});
httpBackend.when("GET", "/sync").respond(200, syncData);
return Promise.all([
client.syncLeftRooms().then(function() {
const room = client.getRoom(roomTwo);
const tok = room.getLiveTimeline().getPaginationToken(
EventTimeline.BACKWARDS);
expect(tok).toEqual("pagTok");
}),
// first flush the filter request; this will make syncLeftRooms make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flushAllExpected();
}),
]);
});
});
/**
* waits for the MatrixClient to emit one or more 'sync' events.
*
* @param {Number?} numSyncs number of syncs to wait for
* @returns {Promise} promise which resolves after the sync events have happened
*/
function awaitSyncEvent(numSyncs) {
return utils.syncPromise(client, numSyncs);
}
});
File diff suppressed because it is too large Load Diff
+170
View File
@@ -0,0 +1,170 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Account } from "@matrix-org/olm";
import { logger } from "../../src/logger";
import { decodeRecoveryKey } from "../../src/crypto/recoverykey";
import { IKeyBackupInfo, IKeyBackupSession } from "../../src/crypto/keybackup";
import { TestClient } from "../TestClient";
import { IEvent } from "../../src";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
const ROOM_ID = '!ROOM:ID';
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
const ENCRYPTED_EVENT: Partial<IEvent> = {
type: 'm.room.encrypted',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: 'SENDER_CURVE25519',
session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
},
room_id: '!ROOM:ID',
event_id: '$event1',
origin_server_ts: 1507753886000,
};
const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
},
};
const CURVE25519_BACKUP_INFO: IKeyBackupInfo = {
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
const RECOVERY_KEY = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
/**
* start an Olm session with a given recipient
*/
function createOlmSession(olmAccount: Olm.Account, recipientTestClient: TestClient): Promise<Olm.Session> {
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
);
return session;
});
}
describe("megolm key backups", function() {
if (!global.Olm) {
logger.warn('not running megolm tests: Olm not present');
return;
}
const Olm = global.Olm;
let testOlmAccount: Olm.Account;
let aliceTestClient: TestClient;
const setupTestClient = (): [Account, TestClient] => {
const aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
const testOlmAccount = new Olm.Account();
testOlmAccount!.create();
return [testOlmAccount, aliceTestClient];
};
beforeAll(function() {
return Olm.init();
});
beforeEach(async function() {
[testOlmAccount, aliceTestClient] = setupTestClient();
await aliceTestClient!.client.initCrypto();
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
});
afterEach(function() {
return aliceTestClient!.stop();
});
it("Alice checks key backups when receiving a message she can't decrypt", function() {
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [ENCRYPTED_EVENT],
},
};
return aliceTestClient!.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then(() => {
const privkey = decodeRecoveryKey(RECOVERY_KEY);
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
}).then(() => {
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient!.expectKeyBackupQuery(
ROOM_ID,
SESSION_ID,
200,
CURVE25519_KEY_BACKUP_DATA,
);
return aliceTestClient!.httpBackend.flushAllExpected();
}).then(function(): Promise<MatrixEvent> {
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
if (event.getContent()) {
return Promise.resolve(event);
}
return new Promise((resolve, reject) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}).then((event) => {
expect(event.getContent()).toEqual('testytest');
});
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+813
View File
@@ -0,0 +1,813 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// eslint-disable-next-line no-restricted-imports
import MockHttpBackend from "matrix-mock-request";
import { fail } from "assert";
import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Extension } from "../../src/sliding-sync";
import { TestClient } from "../TestClient";
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
import {
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
} from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend | undefined;
let sdk: SlidingSyncSdk | undefined;
let mockSlidingSync: SlidingSync | undefined;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getList = jest.fn();
s.getListData = jest.fn();
s.getRoomSubscriptions = jest.fn();
s.listLength = jest.fn();
s.modifyRoomSubscriptionInfo = jest.fn();
s.modifyRoomSubscriptions = jest.fn();
s.registerExtension = jest.fn();
s.setList = jest.fn();
s.setListRanges = jest.fn();
s.start = jest.fn();
s.stop = jest.fn();
s.resend = jest.fn();
return s;
};
// shorthand way to make events without filling in all the fields
let eventIdCounter = 0;
const mkOwnEvent = (evType: string, content: object): IRoomEvent => {
eventIdCounter++;
return {
type: evType,
content: content,
sender: selfUserId,
origin_server_ts: Date.now(),
event_id: "$" + eventIdCounter,
};
};
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
eventIdCounter++;
return {
type: evType,
state_key: stateKey,
content: content,
sender: selfUserId,
origin_server_ts: Date.now(),
event_id: "$" + eventIdCounter,
};
};
const assertTimelineEvents = (got: MatrixEvent[], want: IRoomEvent[]): void => {
expect(got.length).toEqual(want.length);
got.forEach((m, i) => {
expect(m.getType()).toEqual(want[i].type);
expect(m.getSender()).toEqual(want[i].sender);
expect(m.getId()).toEqual(want[i].event_id);
expect(m.getContent()).toEqual(want[i].content);
expect(m.getTs()).toEqual(want[i].origin_server_ts);
if (want[i].unsigned) {
expect(m.getUnsigned()).toEqual(want[i].unsigned);
}
const maybeStateEvent = want[i] as IStateEvent;
if (maybeStateEvent.state_key) {
expect(m.getStateKey()).toEqual(maybeStateEvent.state_key);
}
});
};
// assign client/httpBackend globals
const setupClient = async (testOpts?: Partial<IStoredClientOpts&{withCrypto: boolean}>) => {
testOpts = testOpts || {};
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto();
testOpts.crypto = client!.crypto;
}
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
};
// tear down client/httpBackend globals
const teardownClient = () => {
client!.stopClient();
return httpBackend!.stop();
};
// find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension => {
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
// find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension;
if (calledExtension && calledExtension.name() === name) {
return calledExtension;
}
}
fail("cannot find extension " + name);
};
describe("sync/stop", () => {
beforeAll(async () => {
await setupClient();
});
afterAll(teardownClient);
it("can sync()", async () => {
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
expect(mockSlidingSync!.start).toBeCalled();
});
it("can stop()", async () => {
sdk!.stop();
expect(mockSlidingSync!.stop).toBeCalled();
});
});
describe("rooms", () => {
beforeAll(async () => {
await setupClient();
});
afterAll(teardownClient);
describe("initial", () => {
beforeAll(async () => {
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
});
// inject some rooms with different fields set.
// All rooms are new so they all have initial: true
const roomA = "!a_state_and_timeline:localhost";
const roomB = "!b_timeline_only:localhost";
const roomC = "!c_with_highlight_count:localhost";
const roomD = "!d_with_notif_count:localhost";
const roomE = "!e_with_invite:localhost";
const roomF = "!f_calc_room_name:localhost";
const roomG = "!g_join_invite_counts:localhost";
const data: Record<string, MSC3575RoomData> = {
[roomA]: {
name: "A",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
],
timeline: [
mkOwnEvent(EventType.RoomMessage, { body: "hello A" }),
mkOwnEvent(EventType.RoomMessage, { body: "world A" }),
],
initial: true,
},
[roomB]: {
name: "B",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
],
initial: true,
},
[roomC]: {
name: "C",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
mkOwnEvent(EventType.RoomMessage, { body: "world C" }),
],
highlight_count: 5,
initial: true,
},
[roomD]: {
name: "D",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
mkOwnEvent(EventType.RoomMessage, { body: "world D" }),
],
notification_count: 5,
initial: true,
},
[roomE]: {
name: "E",
required_state: [],
timeline: [],
invite_state: [
{
type: EventType.RoomMember,
content: { membership: "invite" },
state_key: selfUserId,
sender: "@bob:localhost",
event_id: "$room_e_invite",
origin_server_ts: 123456,
},
{
type: "m.room.join_rules",
content: { join_rule: "invite" },
state_key: "",
sender: "@bob:localhost",
event_id: "$room_e_join_rule",
origin_server_ts: 123456,
},
],
initial: true,
},
[roomF]: {
name: "#foo:localhost",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "This should be ignored" }, ""),
],
timeline: [
mkOwnEvent(EventType.RoomMessage, { body: "hello A" }),
mkOwnEvent(EventType.RoomMessage, { body: "world A" }),
],
initial: true,
},
[roomG]: {
name: "G",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
],
joined_count: 5,
invited_count: 2,
initial: true,
},
};
it("can be created with required_state and timeline", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomA].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
});
it("can be created with timeline only", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomB].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
});
it("can be created with a highlight_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(data[roomC].highlight_count);
});
it("can be created with a notification_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(data[roomD].notification_count);
});
it("can be created with an invited/joined_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
});
it("can be created with invite_state", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
});
it("uses the 'name' field to caluclate the room name", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
const gotRoom = client!.getRoom(roomF);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.name,
).toEqual(data[roomF].name);
});
describe("updating", () => {
it("can update with a new timeline event", async () => {
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
timeline: [newEvent],
required_state: [],
name: data[roomA].name,
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
});
it("can update with a new required_state event", async () => {
let gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
required_state: [
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
],
timeline: [],
name: data[roomB].name,
});
gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
});
it("can update with a new highlight_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, {
name: data[roomC].name,
required_state: [],
timeline: [],
highlight_count: 1,
});
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(1);
});
it("can update with a new notification_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, {
name: data[roomD].name,
required_state: [],
timeline: [],
notification_count: 1,
});
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(1);
});
it("can update with a new joined_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, {
name: data[roomD].name,
required_state: [],
timeline: [],
joined_count: 1,
});
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
});
// Regression test for a bug which caused the timeline entries to be out-of-order
// when the same room appears twice with different timeline limits. E.g appears in
// the list with timeline_limit:1 then appears again as a room subscription with
// timeline_limit:50
it("can return history with a larger timeline_limit", async () => {
const timeline = data[roomA].timeline;
const oldTimeline = [
mkOwnEvent(EventType.RoomMessage, { body: "old event A" }),
mkOwnEvent(EventType.RoomMessage, { body: "old event B" }),
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
...timeline,
];
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
timeline: oldTimeline,
required_state: [],
name: data[roomA].name,
initial: true, // e.g requested via room subscription
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
(e) => (e.getType() + " : " + e.getContent().body)),
);
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline);
});
});
});
});
describe("lifecycle", () => {
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
});
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null,
);
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
);
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
);
}
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
});
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle,
SlidingSyncState.Complete,
{ pos: "i", lists: [], rooms: {}, extensions: {} },
null,
);
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
});
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
expect(mockSlidingSync!.stop).not.toBeCalled();
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
errcode: "M_UNKNOWN_TOKEN",
message: "Oh no your access token is no longer valid",
}));
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
expect(mockSlidingSync!.stop).toBeCalled();
});
});
describe("opts", () => {
afterEach(teardownClient);
it("can resolveProfilesToInvites", async () => {
await setupClient({
resolveInvitesToProfiles: true,
});
const roomId = "!resolveProfilesToInvites:localhost";
const invitee = "@invitee:localhost";
const inviteeProfile = {
avatar_url: "mxc://foobar",
displayname: "The Invitee",
};
httpBackend!.when("GET", "/profile").respond(200, inviteeProfile);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
initial: true,
name: "Room with Invite",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
],
});
await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name);
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
const inviteeMember = room.getMember(invitee)!;
expect(inviteeMember).toBeDefined();
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
});
});
describe("ExtensionE2EE", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient({
withCrypto: true,
});
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("e2ee");
});
afterAll(async () => {
// needed else we do some async operations in the background which can cause Jest to whine:
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
// Attempted to log "Saving device tracking data null"."
client!.crypto!.stop();
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("can update device lists", () => {
ext.onResponse({
device_lists: {
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
},
});
// TODO: more assertions?
});
it("can update OTK counts", () => {
client!.crypto!.updateOneTimeKeyCount = jest.fn();
ext.onResponse({
device_one_time_keys_count: {
signed_curve25519: 42,
},
});
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
ext.onResponse({
device_one_time_keys_count: {
not_signed_curve25519: 42,
// missing field -> default to 0
},
});
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
});
it("can update fallback keys", () => {
ext.onResponse({
device_unused_fallback_key_types: ["signed_curve25519"],
});
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
ext.onResponse({
device_unused_fallback_key_types: ["not_signed_curve25519"],
});
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
});
});
describe("ExtensionAccountData", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("account_data");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes global account data", async () => {
const globalType = "global_test";
const globalContent = {
info: "here",
};
let globalData = client!.getAccountData(globalType);
expect(globalData).toBeUndefined();
ext.onResponse({
global: [
{
type: globalType,
content: globalContent,
},
],
});
globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeDefined();
expect(globalData.getContent()).toEqual(globalContent);
});
it("processes rooms account data", async () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with account data",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
initial: true,
});
const roomContent = {
foo: "bar",
};
const roomType = "test";
ext.onResponse({
rooms: {
[roomId]: [
{
type: roomType,
content: roomContent,
},
],
},
});
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
const event = room.getAccountData(roomType)!;
expect(event).toBeDefined();
expect(event.getContent()).toEqual(roomContent);
});
it("doesn't crash for unknown room account data", async () => {
const unknownRoomId = "!unknown:id";
const roomType = "tester";
ext.onResponse({
rooms: {
[unknownRoomId]: [
{
type: roomType,
content: {
foo: "Bar",
},
},
],
},
});
const room = client!.getRoom(unknownRoomId);
expect(room).toBeNull();
expect(client!.getAccountData(roomType)).toBeUndefined();
});
it("can update push rules via account data", async () => {
const roomId = "!foo:bar";
const pushRulesContent: IPushRules = {
global: {
[PushRuleKind.RoomSpecific]: [{
enabled: true,
default: true,
pattern: "monkey",
actions: [
{
set_tweak: TweakName.Sound,
value: "default",
},
],
rule_id: roomId,
}],
},
};
let pushRule = client!.getRoomPushRule("global", roomId);
expect(pushRule).toBeUndefined();
ext.onResponse({
global: [
{
type: EventType.PushRules,
content: pushRulesContent,
},
],
});
pushRule = client!.getRoomPushRule("global", roomId)!;
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
});
});
describe("ExtensionToDevice", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("to_device");
});
it("gets enabled with a limit on the initial request only", () => {
const reqJson: any = ext.onRequest(true);
expect(reqJson.enabled).toEqual(true);
expect(reqJson.limit).toBeGreaterThan(0);
expect(reqJson.since).toBeUndefined();
});
it("updates the since value", async () => {
ext.onResponse({
next_batch: "12345",
events: [],
});
expect(ext.onRequest(false)).toEqual({
since: "12345",
});
});
it("can handle missing fields", async () => {
ext.onResponse({
next_batch: "23456",
// no events array
});
});
it("emits to-device events on the client", async () => {
const toDeviceType = "custom_test";
const toDeviceContent = {
foo: "bar",
};
let called = false;
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
expect(ev.getContent()).toEqual(toDeviceContent);
expect(ev.getType()).toEqual(toDeviceType);
called = true;
});
ext.onResponse({
next_batch: "34567",
events: [
{
type: toDeviceType,
content: toDeviceContent,
},
],
});
expect(called).toBe(true);
});
it("can cancel key verification requests", async () => {
const seen: Record<string, boolean> = {};
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
const evType = ev.getType();
expect(seen[evType]).toBeFalsy();
seen[evType] = true;
if (evType === "m.key.verification.start" || evType === "m.key.verification.request") {
expect(ev.isCancelled()).toEqual(true);
} else {
expect(ev.isCancelled()).toEqual(false);
}
});
ext.onResponse({
next_batch: "45678",
events: [
// someone tries to verify keys
{
type: "m.key.verification.start",
content: {
transaction_id: "a",
},
},
{
type: "m.key.verification.request",
content: {
transaction_id: "a",
},
},
// then gives up
{
type: "m.key.verification.cancel",
content: {
transaction_id: "a",
},
},
],
});
});
});
});
File diff suppressed because it is too large Load Diff
+1 -9
View File
@@ -16,20 +16,12 @@ limitations under the License.
*/
import { logger } from '../src/logger';
import * as utils from "../src/utils";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
global.Olm = require('@matrix-org/olm');
logger.log('loaded libolm');
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available");
}
// also try to set node crypto
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support: some tests will fail');
}
+19
View File
@@ -0,0 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import DOMException from "domexception";
global.DOMException = DOMException;
+8 -3
View File
@@ -27,6 +27,7 @@ type InfoContentProps = {
isLive?: boolean;
assetType?: LocationAssetType;
description?: string;
timestamp?: number;
};
const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = {
timeout: 3600000,
@@ -44,7 +45,11 @@ export const makeBeaconInfoEvent = (
eventId?: string,
): MatrixEvent => {
const {
timeout, isLive, description, assetType,
timeout,
isLive,
description,
assetType,
timestamp,
} = {
...DEFAULT_INFO_CONTENT_PROPS,
...contentProps,
@@ -53,10 +58,10 @@ export const makeBeaconInfoEvent = (
type: M_BEACON_INFO.name,
room_id: roomId,
state_key: sender,
content: makeBeaconInfoContent(timeout, isLive, description, assetType),
content: makeBeaconInfoContent(timeout, isLive, description, assetType, timestamp),
});
event.event.origin_server_ts = Date.now();
event.event.origin_server_ts = timestamp || Date.now();
// live beacons use the beacon_info event id
// set or default this
+94
View File
@@ -0,0 +1,94 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MethodKeysOf, mocked, MockedObject } from "jest-mock";
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { User } from "../../src/models/user";
/**
* Mock client with real event emitter
* useful for testing code that listens
* to MatrixClient events
*/
export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
constructor(mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>> = {}) {
super();
Object.assign(this, mockProperties);
}
}
/**
* - make a mock client
* - cast the type to mocked(MatrixClient)
* - spy on MatrixClientPeg.get to return the mock
* eg
* ```
* const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId),
});
* ```
*/
export const getMockClientWithEventEmitter = (
mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>>,
): MockedObject<MatrixClient> => {
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
return mock;
};
/**
* Returns basic mocked client methods related to the current user
* ```
* const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser('@mytestuser:domain'),
});
* ```
*/
export const mockClientMethodsUser = (userId = '@alice:domain') => ({
getUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
});
/**
* Returns basic mocked client methods related to rendering events
* ```
* const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser('@mytestuser:domain'),
});
* ```
*/
export const mockClientMethodsEvents = () => ({
decryptEventIfNeeded: jest.fn(),
getPushActionsForEvent: jest.fn(),
});
/**
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodKeysOf<MatrixClient>, unknown>> => ({
doesServerSupportSeparateAddAndBind: jest.fn(),
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
});
+1 -1
View File
@@ -24,5 +24,5 @@ limitations under the License.
* expect(beaconLivenessEmits.length).toBe(1);
* ```
*/
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
spy.mock.calls.filter((args) => args[0] === eventType);
+107 -13
View File
@@ -6,7 +6,7 @@ import '../olm-loader';
import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, MatrixClient } from "../../src";
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
@@ -70,11 +70,11 @@ export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
interface IEventOpts {
type: EventType | string;
room: string;
room?: string;
sender?: string;
skey?: string;
content: IContent;
event?: boolean;
prev_content?: IContent;
user?: string;
unsigned?: IUnsigned;
redacts?: string;
@@ -93,7 +93,9 @@ let testEventIndex = 1; // counter for events, easier for comparison of randomly
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts, client?: MatrixClient): object | MatrixEvent {
export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixClient): Partial<IEvent> | MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
@@ -102,6 +104,7 @@ export function mkEvent(opts: IEventOpts, client?: MatrixClient): object | Matri
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
prev_content: opts.prev_content,
unsigned: opts.unsigned || {},
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
@@ -128,12 +131,27 @@ export function mkEvent(opts: IEventOpts, client?: MatrixClient): object | Matri
return opts.event ? new MatrixEvent(event) : event;
}
type GeneratedMetadata = {
event_id: string;
txn_id: string;
origin_server_ts: number;
};
export function mkEventCustom<T>(base: T): T & GeneratedMetadata {
return {
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
origin_server_ts: Date.now(),
...base,
};
}
interface IPresenceOpts {
user?: string;
sender?: string;
url: string;
name: string;
ago: number;
url?: string;
name?: string;
ago?: number;
presence?: string;
event?: boolean;
}
@@ -143,7 +161,9 @@ interface IPresenceOpts {
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts: IPresenceOpts): object | MatrixEvent {
export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent;
export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial<IEvent>;
export function mkPresence(opts: IPresenceOpts & { event?: boolean }): Partial<IEvent> | MatrixEvent {
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
@@ -159,7 +179,7 @@ export function mkPresence(opts: IPresenceOpts): object | MatrixEvent {
}
interface IMembershipOpts {
room: string;
room?: string;
mship: string;
sender?: string;
user?: string;
@@ -182,7 +202,9 @@ interface IMembershipOpts {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts: IMembershipOpts): object | MatrixEvent {
export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent;
export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial<IEvent>;
export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Partial<IEvent> | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMember,
@@ -203,8 +225,20 @@ export function mkMembership(opts: IMembershipOpts): object | MatrixEvent {
return mkEvent(eventOpts);
}
export function mkMembershipCustom<T>(
base: T & { membership: string, sender: string, content?: IContent },
): T & { type: EventType, sender: string, state_key: string, content: IContent } & GeneratedMetadata {
const content = base.content || {};
return mkEventCustom({
...base,
content: { ...content, membership: base.membership },
type: EventType.RoomMember,
state_key: base.sender,
});
}
interface IMessageOpts {
room: string;
room?: string;
user: string;
msg?: string;
event?: boolean;
@@ -220,12 +254,17 @@ interface IMessageOpts {
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts: IMessageOpts, client?: MatrixClient): object | MatrixEvent {
export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
export function mkMessage(
opts: IMessageOpts & { event?: boolean },
client?: MatrixClient,
): Partial<IEvent> | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
content: {
msgtype: "m.text",
msgtype: MsgType.Text,
body: opts.msg,
},
};
@@ -236,6 +275,50 @@ export function mkMessage(opts: IMessageOpts, client?: MatrixClient): object | M
return mkEvent(eventOpts, client);
}
interface IReplyMessageOpts extends IMessageOpts {
replyToMessage: MatrixEvent;
}
/**
* Create a reply message.
*
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {MatrixEvent} opts.replyToMessage The replied message
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
*/
export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
export function mkReplyMessage(
opts: IReplyMessageOpts & { event?: boolean },
client?: MatrixClient,
): Partial<IEvent> | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
content: {
"msgtype": MsgType.Text,
"body": opts.msg,
"m.relates_to": {
"rel_type": "m.in_reply_to",
"event_id": opts.replyToMessage.getId(),
"m.in_reply_to": {
"event_id": opts.replyToMessage.getId(),
},
},
},
};
if (!eventOpts.content.body) {
eventOpts.content.body = "Random->" + Math.random();
}
return mkEvent(eventOpts, client);
}
/**
* A mock implementation of webstorage
*
@@ -290,3 +373,14 @@ export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent>
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",
data: {},
device_display_name: "name",
kind: "http",
lang: "en",
pushkey: "pushpush",
...extra,
});
+146
View File
@@ -0,0 +1,146 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export const DUMMY_SDP = (
"v=0\r\n" +
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
"a=ice-options:trickle\r\n" +
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
"a=setup:active\r\n" +
"a=mid:0\r\n" +
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
"a=sendrecv\r\n" +
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
"a=rtcp-mux\r\n" +
"a=rtpmap:111 opus/48000/2\r\n" +
"a=rtcp-fb:111 transport-cc\r\n" +
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
"a=rtpmap:103 ISAC/16000\r\n" +
"a=rtpmap:104 ISAC/32000\r\n" +
"a=rtpmap:9 G722/8000\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:8 PCMA/8000\r\n" +
"a=rtpmap:106 CN/32000\r\n" +
"a=rtpmap:105 CN/16000\r\n" +
"a=rtpmap:13 CN/8000\r\n" +
"a=rtpmap:110 telephone-event/48000\r\n" +
"a=rtpmap:112 telephone-event/32000\r\n" +
"a=rtpmap:113 telephone-event/16000\r\n" +
"a=rtpmap:126 telephone-event/8000\r\n" +
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
);
export class MockRTCPeerConnection {
localDescription: RTCSessionDescription;
constructor() {
this.localDescription = {
sdp: DUMMY_SDP,
type: 'offer',
toJSON: function() { },
};
}
addEventListener() { }
createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
createOffer() {
return Promise.resolve({});
}
setRemoteDescription() {
return Promise.resolve();
}
setLocalDescription() {
return Promise.resolve();
}
close() { }
getStats() { return []; }
addTrack(track: MockMediaStreamTrack) { return new MockRTCRtpSender(track); }
}
export class MockRTCRtpSender {
constructor(public track: MockMediaStreamTrack) { }
replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
}
export class MockMediaStreamTrack {
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
stop() { }
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
export class MockMediaStream {
constructor(
public id: string,
private tracks: MockMediaStreamTrack[] = [],
) {}
listeners: [string, (...args: any[]) => any][] = [];
dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
getTracks() { return this.tracks; }
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
addTrack(track: MockMediaStreamTrack) {
this.tracks.push(track);
this.dispatchEvent("addtrack");
}
removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
}
export class MockMediaDeviceInfo {
constructor(
public kind: "audio" | "video",
) { }
}
export class MockMediaHandler {
getUserMediaStream(audio: boolean, video: boolean) {
const tracks = [];
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
return new MockMediaStream("mock_stream_from_media_handler", tracks);
}
stopUserMediaStream() { }
hasAudioDevice() { return true; }
}
+5
View File
@@ -21,18 +21,21 @@ describe("NamespacedValue", () => {
const ns = new NamespacedValue("stable", "unstable");
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBe(ns.unstable);
expect(ns.names).toEqual([ns.stable, ns.unstable]);
});
it("should return unstable if there is no stable", () => {
const ns = new NamespacedValue(null, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
});
it("should have a falsey unstable if needed", () => {
const ns = new NamespacedValue("stable", null);
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.stable]);
});
it("should match against either stable or unstable", () => {
@@ -58,12 +61,14 @@ describe("UnstableValue", () => {
const ns = new UnstableValue("stable", "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBe(ns.stable);
expect(ns.names).toEqual([ns.unstable, ns.stable]);
});
it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
});
it("should not permit falsey unstable values", () => {
@@ -1,6 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,19 +17,19 @@ limitations under the License.
import MockHttpBackend from "matrix-mock-request";
import * as sdk from "../../src";
import { AutoDiscovery } from "../../src/autodiscovery";
describe("AutoDiscovery", function() {
let httpBackend = null;
beforeEach(function() {
httpBackend = new MockHttpBackend();
sdk.request(httpBackend.requestFn);
});
const getHttpBackend = (): MockHttpBackend => {
const httpBackend = new MockHttpBackend();
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
return httpBackend;
};
it("should throw an error when no domain is specified", function() {
getHttpBackend();
return Promise.all([
// @ts-ignore testing no args
AutoDiscovery.findClientConfig(/* no args */).then(() => {
throw new Error("Expected a failure, not success with no args");
}, () => {
@@ -42,13 +42,13 @@ describe("AutoDiscovery", function() {
return true;
}),
AutoDiscovery.findClientConfig(null).then(() => {
AutoDiscovery.findClientConfig(null as any).then(() => {
throw new Error("Expected a failure, not success with null");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(true).then(() => {
AutoDiscovery.findClientConfig(true as any).then(() => {
throw new Error("Expected a failure, not success with a non-string");
}, () => {
return true;
@@ -57,6 +57,7 @@ describe("AutoDiscovery", function() {
});
it("should return PROMPT when .well-known 404s", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
return Promise.all([
httpBackend.flushAllExpected(),
@@ -80,6 +81,7 @@ describe("AutoDiscovery", function() {
});
it("should return FAIL_PROMPT when .well-known returns a 500 error", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
return Promise.all([
httpBackend.flushAllExpected(),
@@ -103,6 +105,7 @@ describe("AutoDiscovery", function() {
});
it("should return FAIL_PROMPT when .well-known returns a 400 error", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
return Promise.all([
httpBackend.flushAllExpected(),
@@ -126,6 +129,7 @@ describe("AutoDiscovery", function() {
});
it("should return FAIL_PROMPT when .well-known returns an empty body", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
return Promise.all([
httpBackend.flushAllExpected(),
@@ -148,31 +152,31 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
it("should return FAIL_PROMPT when .well-known returns not-JSON", async () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc", true);
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
AutoDiscovery.findClientConfig("example.org").then(
expect(expected).toEqual,
),
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (empty string)", function() {
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (empty string)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "",
@@ -199,8 +203,8 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (no property)", function() {
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (no property)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {},
});
@@ -225,8 +229,8 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (disallowed scheme)", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for m.homeserver (disallowed scheme)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "mxc://example.org",
@@ -255,6 +259,7 @@ describe("AutoDiscovery", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 404)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -284,6 +289,7 @@ describe("AutoDiscovery", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 500)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -313,6 +319,7 @@ describe("AutoDiscovery", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 200 but wrong content)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
not_matrix_versions: ["r0.0.1"],
});
@@ -344,8 +351,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " +
"m.homeserver", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri).toEqual("https://example.org/_matrix/client/versions");
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
@@ -376,8 +384,9 @@ describe("AutoDiscovery", function() {
});
it("should return SUCCESS with the right homeserver URL", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -411,8 +420,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (missing base_url)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -451,8 +461,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (empty base_url)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -491,8 +502,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (validation error: 404)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -532,8 +544,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (validation error: 500)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -573,14 +586,15 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS when the identity server configuration is " +
"verifiably accurate", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
@@ -615,14 +629,15 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS and preserve non-standard keys from the " +
".well-known response", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
@@ -660,4 +675,76 @@ describe("AutoDiscovery", function() {
}),
]);
});
it("should return FAIL_PROMPT for connection errors", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined);
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT for fetch errors", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, new Error("CORS or something"));
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT for invalid JSON", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "<html>", true);
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
});
+133 -1
View File
@@ -17,7 +17,14 @@ limitations under the License.
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
import { M_TOPIC } from "../../src/@types/topic";
import {
makeBeaconContent,
makeBeaconInfoContent,
makeTopicContent,
parseBeaconContent,
parseTopicContent,
} from "../../src/content-helpers";
describe('Beacon content helpers', () => {
describe('makeBeaconInfoContent()', () => {
@@ -121,4 +128,129 @@ describe('Beacon content helpers', () => {
});
});
});
describe("parseBeaconContent()", () => {
it("should not explode when parsing an invalid beacon", () => {
// deliberate cast to simulate wire content being invalid
const result = parseBeaconContent({} as any);
expect(result).toEqual({
description: undefined,
uri: undefined,
timestamp: undefined,
});
});
it("should parse unstable values", () => {
const uri = "urigoeshere";
const description = "descriptiongoeshere";
const timestamp = 1234;
const result = parseBeaconContent({
"org.matrix.msc3488.location": {
uri,
description,
},
"org.matrix.msc3488.ts": timestamp,
// relationship not used - just here to satisfy types
"m.relates_to": {
rel_type: "m.reference",
event_id: "$unused",
},
});
expect(result).toEqual({
description,
uri,
timestamp,
});
});
it("should parse stable values", () => {
const uri = "urigoeshere";
const description = "descriptiongoeshere";
const timestamp = 1234;
const result = parseBeaconContent({
"m.location": {
uri,
description,
},
"m.ts": timestamp,
// relationship not used - just here to satisfy types
"m.relates_to": {
rel_type: "m.reference",
event_id: "$unused",
},
});
expect(result).toEqual({
description,
uri,
timestamp,
});
});
});
});
describe('Topic content helpers', () => {
describe('makeTopicContent()', () => {
it('creates fully defined event content without html', () => {
expect(makeTopicContent("pizza")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}],
});
});
it('creates fully defined event content with html', () => {
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}, {
body: "<b>pizza</b>",
mimetype: "text/html",
}],
});
});
});
describe('parseTopicContent()', () => {
it('parses event content with plain text topic without mimetype', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
}],
})).toEqual({
text: "pizza",
});
});
it('parses event content with plain text topic', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}],
})).toEqual({
text: "pizza",
});
});
it('parses event content with html topic', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "<b>pizza</b>",
mimetype: "text/html",
}],
})).toEqual({
text: "pizza",
html: "<b>pizza</b>",
});
});
});
});
@@ -19,41 +19,41 @@ describe("ContentRepo", function() {
});
it("should return a download URL if no width/height/resize are specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
);
});
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
);
});
it("should return the empty string for null input", function() {
expect(getHttpUriForMxc(null)).toEqual("");
expect(getHttpUriForMxc(null as any, '')).toEqual("");
});
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop",
);
});
);
});
it("should put fragments from mxc:// URIs after any query parameters",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32#automade",
);
});
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
);
});
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
);
});
});
});
-428
View File
@@ -1,428 +0,0 @@
import '../olm-loader';
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { Crypto } from "../../src/crypto";
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../MockStorageApi";
import { TestClient } from "../TestClient";
import { MatrixEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import * as olmlib from "../../src/crypto/olmlib";
import { sleep } from "../../src/utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from '../../src/logger';
const Olm = global.Olm;
describe("Crypto", function() {
if (!CRYPTO_ENABLED) {
return;
}
beforeAll(function() {
return Olm.init();
});
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(3);
});
describe("encrypted events", function() {
it("provides encryption information", async function() {
const client = (new TestClient(
"@alice:example.com", "deviceid",
)).client;
await client.initCrypto();
// unencrypted event
const event = {
getId: () => "$event_id",
getSenderKey: () => null,
getWireContent: () => {return {};},
};
let encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeFalsy();
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
event.getForwardingCurve25519KeyChain = () => ["not empty"];
event.isKeySourceUntrusted = () => false;
event.getClaimedEd25519Key =
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
expect(encryptionInfo.authenticated).toBeFalsy();
expect(encryptionInfo.sender).toBeFalsy();
// known sender, megolm key from backup
event.getForwardingCurve25519KeyChain = () => [];
event.isKeySourceUntrusted = () => true;
const device = new DeviceInfo("FLIBBLE");
device.keys["curve25519:FLIBBLE"] =
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client.crypto.deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
expect(encryptionInfo.authenticated).toBeFalsy();
expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeFalsy();
// known sender, trusted megolm key, but bad ed25519key
event.isKeySourceUntrusted = () => false;
device.keys["ed25519:FLIBBLE"] =
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
expect(encryptionInfo.authenticated).toBeTruthy();
expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeTruthy();
client.stopClient();
});
});
describe('Session management', function() {
const otkResponse = {
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:FLIBBLE': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally a valid signature',
},
},
},
},
},
},
};
let crypto;
let mockBaseApis;
let mockRoomList;
let fakeEmitter;
beforeEach(async function() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
cryptoStore.storeEndToEndDeviceData({
devices: {
'@bob:home.server': {
'BOBDEVICE': {
keys: {
'curve25519:BOBDEVICE': 'this is a key',
},
},
},
},
trackingStatus: {},
});
mockBaseApis = {
sendToDevice: jest.fn(),
getKeyBackupVersion: jest.fn(),
isGuest: jest.fn(),
};
mockRoomList = {};
fakeEmitter = new EventEmitter();
crypto = new Crypto(
mockBaseApis,
sessionStore,
"@alice:home.server",
"FLIBBLE",
sessionStore,
cryptoStore,
mockRoomList,
);
crypto.registerEventHandlers(fakeEmitter);
await crypto.init();
});
afterEach(async function() {
await crypto.stop();
});
it("restarts wedged Olm sessions", async function() {
const prom = new Promise((resolve) => {
mockBaseApis.claimOneTimeKeys = function() {
resolve();
return otkResponse;
};
});
fakeEmitter.emit('toDeviceEvent', {
getId: jest.fn().mockReturnValue("$wedged"),
getType: jest.fn().mockReturnValue('m.room.message'),
getContent: jest.fn().mockReturnValue({
msgtype: 'm.bad.encrypted',
}),
getWireContent: jest.fn().mockReturnValue({
algorithm: 'm.olm.v1.curve25519-aes-sha2',
sender_key: 'this is a key',
}),
getSender: jest.fn().mockReturnValue('@bob:home.server'),
});
await prom;
});
});
describe('Key requests', function() {
let aliceClient;
let bobClient;
beforeEach(async function() {
aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await aliceClient.initCrypto();
await bobClient.initCrypto();
});
afterEach(async function() {
aliceClient.stopClient();
bobClient.stopClient();
});
it(
"does not cancel keyshare requests if some messages are not decrypted",
async function() {
function awaitEvent(emitter, event) {
return new Promise((resolve, reject) => {
emitter.once(event, (result) => {
resolve(result);
});
});
}
async function keyshareEventForEvent(event, index) {
const eventContent = event.getWireContent();
const key = await aliceClient.crypto.olmDevice
.getInboundGroupSessionKey(
roomId, eventContent.sender_key, eventContent.session_id,
index,
);
const ksEvent = new MatrixEvent({
type: "m.forwarded_room_key",
sender: "@alice:example.com",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: eventContent.sender_key,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
session_id: eventContent.session_id,
session_key: key.key,
chain_index: key.chain_index,
forwarding_curve25519_key_chain:
key.forwarding_curve_key_chain,
},
});
// make onRoomKeyEvent think this was an encrypted event
ksEvent.senderCurve25519Key = "akey";
return ksEvent;
}
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
event.clearEvent = undefined;
event.senderCurve25519Key = null;
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
let eventPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
// keyshare the session key starting at the second message, so
// the first message can't be decrypted yet, but the second one
// can
let ksEvent = await keyshareEventForEvent(events[1], 1);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
const cryptoStore = bobClient.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
session_id: sessionId,
};
// the room key request should still be there, since we haven't
// decrypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toBeDefined();
// keyshare the session key starting at the first message, so
// that it can now be decrypted
eventPromise = awaitEvent(events[0], "Event.decrypted");
ksEvent = await keyshareEventForEvent(events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
await sleep(1);
// the room key request should be gone since we've now decrypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toBeFalsy();
},
);
it("creates a new keyshare request if we request a keyshare", async function() {
// make sure that cancelAndResend... creates a new keyshare request
// if there wasn't an already-existing one
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
const cryptoStore = aliceClient.cryptoStore;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: "!someroom",
session_id: "sessionid",
sender_key: "senderkey",
};
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toBeDefined();
});
it("uses a new txnid for re-requesting keys", async function() {
jest.useFakeTimers();
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
// replace Alice's sendToDevice function with a mock
aliceClient.sendToDevice = jest.fn().mockResolvedValue(undefined);
aliceClient.startClient();
// make a room key request, and record the transaction ID for the
// sendToDevice call
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
// key requests get queued until the sync has finished, but we don't
// let the client set up enough for that to happen, so gut-wrench a bit
// to force it to send now.
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
jest.runAllTimers();
await Promise.resolve();
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
const txnId = aliceClient.sendToDevice.mock.calls[0][2];
// give the room key request manager time to update the state
// of the request
await Promise.resolve();
// cancel and resend the room key request
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
jest.runAllTimers();
await Promise.resolve();
// cancelAndResend will call sendToDevice twice:
// the first call to sendToDevice will be the cancellation
// the second call to sendToDevice will be the key request
expect(aliceClient.sendToDevice).toBeCalledTimes(3);
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
});
});
describe('Secret storage', function() {
it("creates secret storage even if there is no keyInfo", async function() {
jest.spyOn(logger, 'log').mockImplementation(() => {});
jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto();
client.crypto.getSecretStorageKey = async () => null;
client.crypto.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = () => null;
client.crypto.baseApis.setAccountData = () => null;
client.crypto.baseApis.uploadKeySignatures = () => null;
client.crypto.baseApis.http.authedRequest = () => null;
const createSecretStorageKey = async () => {
return {
keyInfo: undefined, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
};
};
await client.crypto.bootstrapSecretStorage({
createSecretStorageKey,
});
});
});
});
File diff suppressed because it is too large Load Diff
@@ -66,23 +66,23 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
});
it.each(types)("should throw if the callback returns falsey",
async ({ type, shouldCache }) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => false,
async ({ type, shouldCache }) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: async () => false as unknown as Uint8Array,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
});
it("should throw if the expected key doesn't come back", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => masterKeyPub,
getCrossSigningKey: async () => masterKeyPub as unknown as Uint8Array,
});
await expect(info.getCrossSigningKey("master", "")).rejects.toThrow();
});
it("should return a key from its callback", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => testKey,
getCrossSigningKey: async () => testKey,
});
const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
@@ -99,7 +99,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
it.each(types)("should request a key from the cache callback (if set)" +
" and does not call app if one is found" +
" %o",
async ({ type, shouldCache }) => {
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
@@ -122,58 +122,58 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
});
it.each(types)("should store a key with the cache callback (if set)",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
if (shouldCache) {
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
}
});
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
if (shouldCache) {
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
}
});
it.each(types)("does not store a bad key to the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
it.each(types)("does not store a value to the cache if it came from the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
new Error("Tried to store a value from cache"),
);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
new Error("Tried to store a value from cache"),
);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
" if one is not found", async ({ type, shouldCache }) => {
@@ -220,12 +220,14 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
*/
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
() => new IndexedDBCryptoStore(undefined, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
// @ts-ignore set private properties
store._backend = new MemoryCryptoStore();
// @ts-ignore
store._backendPromise = Promise.resolve(store._backend);
return store;
}],
@@ -1,7 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,8 +20,10 @@ import { logger } from "../../../src/logger";
import * as utils from "../../../src/utils";
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
import { DeviceList } from "../../../src/crypto/DeviceList";
import { IDownloadKeyResult, MatrixClient } from "../../../src";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
const signedDeviceList = {
const signedDeviceList: IDownloadKeyResult = {
"failures": {},
"device_keys": {
"@test1:sw1v.org": {
@@ -45,13 +47,15 @@ const signedDeviceList = {
"m.megolm.v1.aes-sha2",
],
"device_id": "HGKAWHRVJQ",
"unsigned": {},
"unsigned": {
"device_display_name": "",
},
},
},
},
};
const signedDeviceList2 = {
const signedDeviceList2: IDownloadKeyResult = {
"failures": {},
"device_keys": {
"@test2:sw1v.org": {
@@ -75,7 +79,9 @@ const signedDeviceList2 = {
"m.megolm.v1.aes-sha2",
],
"device_id": "QJVRHWAKGH",
"unsigned": {},
"unsigned": {
"device_display_name": "",
},
},
},
},
@@ -104,10 +110,10 @@ describe('DeviceList', function() {
downloadKeysForUsers: downloadSpy,
getUserId: () => '@test1:sw1v.org',
deviceId: 'HGKAWHRVJQ',
};
} as unknown as MatrixClient;
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
} as unknown as OlmDevice;
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
deviceLists.push(dl);
return dl;
@@ -118,7 +124,7 @@ describe('DeviceList', function() {
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = utils.defer();
const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
@@ -128,6 +134,7 @@ describe('DeviceList', function() {
return prom1.then(() => {
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
dl.stop();
});
});
@@ -137,7 +144,7 @@ describe('DeviceList', function() {
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = utils.defer();
const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
@@ -154,6 +161,7 @@ describe('DeviceList', function() {
dl.saveIfDirty().then(() => {
// the first request completes
queryDefer1.resolve({
failures: {},
device_keys: {
'@test1:sw1v.org': {},
},
@@ -165,11 +173,12 @@ describe('DeviceList', function() {
logger.log("Creating new devicelist to simulate app reload");
downloadSpy.mockReset();
const dl2 = createTestDeviceList();
const queryDefer3 = utils.defer();
const queryDefer3 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
dl2.stop();
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
@@ -178,6 +187,7 @@ describe('DeviceList', function() {
}).then(() => {
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
dl.stop();
});
});
@@ -187,9 +197,9 @@ describe('DeviceList', function() {
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList('@test2:sw1v.org');
const queryDefer1 = utils.defer();
const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
const queryDefer2 = utils.defer();
const queryDefer2 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
@@ -204,6 +214,7 @@ describe('DeviceList', function() {
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
dl.stop();
});
});
});
@@ -1,18 +1,39 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked, MockedObject } from 'jest-mock';
import '../../../olm-loader';
import * as algorithms from "../../../../src/crypto/algorithms";
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../../../MockStorageApi";
import * as testUtils from "../../../test-utils/test-utils";
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
import { Crypto } from "../../../../src/crypto";
import { Crypto, IncomingRoomKeyRequest } from "../../../../src/crypto";
import { logger } from "../../../../src/logger";
import { MatrixEvent } from "../../../../src/models/event";
import { TestClient } from "../../../TestClient";
import { Room } from "../../../../src/models/room";
import * as olmlib from "../../../../src/crypto/olmlib";
import { TypedEventEmitter } from '../../../../src/models/typed-event-emitter';
import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const ROOM_ID = '!ROOM:ID';
@@ -28,17 +49,20 @@ describe("MegolmDecryption", function() {
return Olm.init();
});
let megolmDecryption;
let mockOlmLib;
let mockCrypto;
let mockBaseApis;
let megolmDecryption: algorithms.DecryptionAlgorithm;
let mockOlmLib: MockedObject<typeof olmlib>;
let mockCrypto: MockedObject<Crypto>;
let mockBaseApis: MockedObject<MatrixClient>;
beforeEach(async function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockBaseApis = {};
mockCrypto = testUtils.mock(Crypto, 'Crypto') as MockedObject<Crypto>;
mockBaseApis = {
claimOneTimeKeys: jest.fn(),
sendToDevice: jest.fn(),
queueToDevice: jest.fn(),
} as unknown as MockedObject<MatrixClient>;
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
const cryptoStore = new MemoryCryptoStore();
const olmDevice = new OlmDevice(cryptoStore);
@@ -51,11 +75,15 @@ describe("MegolmDecryption", function() {
});
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
mockOlmLib.encryptMessageForDevice =
jest.fn().mockResolvedValue(undefined);
mockOlmLib = {
encryptMessageForDevice: jest.fn().mockResolvedValue(undefined),
ensureOlmSessionsForDevices: jest.fn(),
} as unknown as MockedObject<typeof olmlib>;
// @ts-ignore illegal assignment that makes these tests work :/
megolmDecryption.olmlib = mockOlmLib;
jest.clearAllMocks();
});
describe('receives some keys:', function() {
@@ -82,12 +110,18 @@ describe("MegolmDecryption", function() {
senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519",
};
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
const mockCrypto = {
decryptEvent: function() {
return Promise.resolve(decryptedData);
},
};
} as unknown as Crypto;
await event.attemptDecryption(mockCrypto).then(() => {
megolmDecryption.onRoomKeyEvent(event);
@@ -115,10 +149,13 @@ describe("MegolmDecryption", function() {
});
it('can respond to a key request event', function() {
const keyRequest = {
const keyRequest: IncomingRoomKeyRequest = {
requestId: '123',
share: jest.fn(),
userId: '@alice:foo',
deviceId: 'alidevice',
requestBody: {
algorithm: '',
room_id: ROOM_ID,
sender_key: "SENDER_CURVE25519",
session_id: groupSession.session_id(),
@@ -131,23 +168,25 @@ describe("MegolmDecryption", function() {
expect(hasKeys).toBe(true);
// set up some pre-conditions for the share call
const deviceInfo = {};
const deviceInfo = {} as DeviceInfo;
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
'@alice:foo': { 'alidevice': {
sessionId: 'alisession',
device: new DeviceInfo('alidevice'),
} },
});
const awaitEncryptForDevice = new Promise((res, rej) => {
const awaitEncryptForDevice = new Promise<void>((res, rej) => {
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
res();
return Promise.resolve();
});
});
mockBaseApis.sendToDevice = jest.fn();
mockBaseApis.sendToDevice.mockReset();
mockBaseApis.queueToDevice.mockReset();
// do the share
megolmDecryption.shareKeysWithDevice(keyRequest);
@@ -257,23 +296,26 @@ describe("MegolmDecryption", function() {
});
describe("session reuse and key reshares", () => {
const rotationPeriodMs = 999 * 24 * 60 * 60 * 1000; // 999 days, so we don't have to deal with it
let megolmEncryption;
let aliceDeviceInfo;
let mockRoom;
let olmDevice;
beforeEach(async () => {
// @ts-ignore assigning to readonly prop
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
const cryptoStore = new MemoryCryptoStore();
olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
mockBaseApis.claimOneTimeKeys.mockResolvedValue({
failures: {},
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
@@ -288,8 +330,9 @@ describe("MegolmDecryption", function() {
},
},
},
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
});
mockBaseApis.sendToDevice.mockResolvedValue(undefined);
mockBaseApis.queueToDevice.mockResolvedValue(undefined);
aliceDeviceInfo = {
deviceId: 'aliceDevice',
@@ -309,18 +352,30 @@ describe("MegolmDecryption", function() {
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
} as DeviceTrustLevel);
megolmEncryption = new MegolmEncryption({
userId: '@user:id',
deviceId: '12345',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
algorithm: 'm.megolm.v1.aes-sha2',
rotation_period_ms: rotationPeriodMs,
},
});
// Splice the real method onto the mock object as megolm uses this method
// on the crypto class in order to encrypt / start sessions
// @ts-ignore Mock
mockCrypto.encryptAndSendToDevices = Crypto.prototype.encryptAndSendToDevices;
// @ts-ignore Mock
mockCrypto.olmDevice = olmDevice;
// @ts-ignore Mock
mockCrypto.baseApis = mockBaseApis;
mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
@@ -329,6 +384,31 @@ describe("MegolmDecryption", function() {
};
});
it("should use larger otkTimeout when preparing to encrypt room", async () => {
megolmEncryption.prepareToEncrypt(mockRoom);
await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 10000,
);
});
it("should generate a new session if this one needs rotation", async () => {
const session = await megolmEncryption.prepareNewSession(false);
session.creationTime -= rotationPeriodMs + 10000; // a smidge over the rotation time
// Inject expired session which needs rotation
megolmEncryption.setupPromise = Promise.resolve(session);
const prepareNewSessionSpy = jest.spyOn(megolmEncryption, "prepareNewSession");
await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(prepareNewSessionSpy).toHaveBeenCalledTimes(1);
});
it("re-uses sessions for sequential messages", async function() {
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
@@ -342,7 +422,7 @@ describe("MegolmDecryption", function() {
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.queueToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
@@ -385,7 +465,7 @@ describe("MegolmDecryption", function() {
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
);
mockBaseApis.sendToDevice.mockClear();
mockBaseApis.queueToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
@@ -393,7 +473,7 @@ describe("MegolmDecryption", function() {
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
expect(mockBaseApis.queueToDevice).not.toHaveBeenCalled();
});
});
});
@@ -422,33 +502,33 @@ describe("MegolmDecryption", function() {
};
const roomId = "!someroom";
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobMember = new RoomMember(roomId, "@bob:example.com");
room.getEncryptionTargetMembers = async function() {
return [{ userId: "@bob:example.com" }];
return [bobMember];
};
room.setBlacklistUnverifiedDevices(true);
aliceClient.store.storeRoom(room);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
const BOB_DEVICES = {
const BOB_DEVICES: Record<string, IDevice> = {
bobdevice1: {
user_id: "@bob:example.com",
device_id: "bobdevice1",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
},
verified: 0,
known: false,
},
bobdevice2: {
user_id: "@bob:example.com",
device_id: "bobdevice2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
},
verified: -1,
known: false,
},
};
@@ -459,32 +539,7 @@ describe("MegolmDecryption", function() {
return this.getDevicesFromStore(userIds);
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice2.session_id;
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice1: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.unverified',
reason:
'The sender has disabled encrypting to unverified devices.',
sender_key: aliceDevice.deviceCurve25519Key,
},
bobdevice2: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.blacklisted',
reason: 'The sender has blocked you.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
};
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
const event = new MatrixEvent({
type: "m.room.message",
@@ -498,7 +553,30 @@ describe("MegolmDecryption", function() {
});
await aliceClient.crypto.encryptEvent(event, room);
expect(run).toBe(true);
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice2.session_id;
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice1: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.unverified',
reason:
'The sender has disabled encrypting to unverified devices.',
sender_key: aliceDevice.deviceCurve25519Key,
},
bobdevice2: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.blacklisted',
reason: 'The sender has blocked you.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
aliceClient.stopClient();
bobClient1.stopClient();
@@ -530,18 +608,16 @@ describe("MegolmDecryption", function() {
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
aliceRoom.getEncryptionTargetMembers = async () => {
return [
{
userId: "@alice:example.com",
membership: "join",
},
{
userId: "@bob:example.com",
membership: "join",
},
];
};
aliceRoom.getEncryptionTargetMembers = jest.fn().mockResolvedValue([
{
userId: "@alice:example.com",
membership: "join",
},
{
userId: "@bob:example.com",
membership: "join",
},
]);
const BOB_DEVICES = {
bobdevice: {
user_id: "@bob:example.com",
@@ -563,30 +639,14 @@ describe("MegolmDecryption", function() {
return this.getDevicesFromStore(userIds);
};
aliceClient.claimOneTimeKeys = async () => {
aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({
// Bob has no one-time keys
return {
one_time_keys: {},
};
};
const sendPromise = new Promise((resolve, reject) => {
aliceClient.sendToDevice = async (msgtype, contentMap) => {
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
resolve();
};
one_time_keys: {},
failures: {},
});
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
const event = new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
@@ -595,7 +655,23 @@ describe("MegolmDecryption", function() {
content: {},
});
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await sendPromise;
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
aliceClient.stopClient();
bobClient.stopClient();
});
it("throws an error describing why it doesn't have a key", async function() {
@@ -611,10 +687,13 @@ describe("MegolmDecryption", function() {
]);
const bobDevice = bobClient.crypto.olmDevice;
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
const roomId = "!someroom";
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
@@ -640,7 +719,7 @@ describe("MegolmDecryption", function() {
},
}))).rejects.toThrow("The sender has blocked you.");
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
@@ -666,6 +745,8 @@ describe("MegolmDecryption", function() {
session_id: "session_id2",
},
}))).rejects.toThrow("The sender has blocked you.");
aliceClient.stopClient();
bobClient.stopClient();
});
it("throws an error describing the lack of an olm session", async function() {
@@ -679,15 +760,19 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
aliceClient.crypto.downloadKeys = async () => {};
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
aliceClient.crypto.downloadKeys = jest.fn();
const bobDevice = bobClient.crypto.olmDevice;
const roomId = "!someroom";
const now = Date.now();
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
@@ -718,7 +803,7 @@ describe("MegolmDecryption", function() {
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
@@ -749,6 +834,8 @@ describe("MegolmDecryption", function() {
},
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
aliceClient.stopClient();
bobClient.stopClient();
});
it("throws an error to indicate a wedged olm session", async function() {
@@ -762,15 +849,18 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto.registerEventHandlers(aliceEventEmitter);
const bobDevice = bobClient.crypto.olmDevice;
aliceClient.crypto.downloadKeys = async () => {};
aliceClient.crypto.downloadKeys = jest.fn();
const roomId = "!someroom";
const now = Date.now();
// pretend we got an event that we can't decrypt
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
content: {
@@ -799,5 +889,7 @@ describe("MegolmDecryption", function() {
},
origin_server_ts: now,
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
aliceClient.stopClient();
bobClient.stopClient();
});
});
@@ -1,6 +1,6 @@
/*
Copyright 2018,2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,17 +15,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from 'jest-mock';
import '../../../olm-loader';
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../../../MockStorageApi";
import { logger } from "../../../../src/logger";
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
import * as olmlib from "../../../../src/crypto/olmlib";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { MatrixClient } from '../../../../src';
function makeOlmDevice() {
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
const cryptoStore = new MemoryCryptoStore();
const olmDevice = new OlmDevice(cryptoStore);
return olmDevice;
}
@@ -51,8 +52,8 @@ describe("OlmDevice", function() {
return global.Olm.init();
});
let aliceOlmDevice;
let bobOlmDevice;
let aliceOlmDevice: OlmDevice;
let bobOlmDevice: OlmDevice;
beforeEach(async function() {
aliceOlmDevice = makeOlmDevice();
@@ -69,7 +70,7 @@ describe("OlmDevice", function() {
bobOlmDevice.deviceCurve25519Key,
sid,
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
) as any; // OlmDevice.encryptMessage has incorrect return type
const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
@@ -96,7 +97,7 @@ describe("OlmDevice", function() {
bobOlmDevice.deviceCurve25519Key,
sessionId,
MESSAGE,
);
) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedOlmDevice = makeOlmDevice();
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
@@ -120,7 +121,7 @@ describe("OlmDevice", function() {
bobOlmDevice.deviceCurve25519Key,
sessionId,
MESSAGE_2,
);
) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedAgainOlmDevice = makeOlmDevice();
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
@@ -148,7 +149,7 @@ describe("OlmDevice", function() {
setTimeout(reject, 500);
});
},
};
} as unknown as MockedObject<MatrixClient>;
const devicesByUser = {
"@bob:example.com": [
DeviceInfo.fromStorage({
@@ -205,7 +206,7 @@ describe("OlmDevice", function() {
setTimeout(reject, 500);
});
},
};
} as unknown as MockedObject<MatrixClient>;
const deviceBobA = DeviceInfo.fromStorage({
keys: {
@@ -15,24 +15,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from "jest-mock";
import '../../olm-loader';
import { logger } from "../../../src/logger";
import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixClient } from "../../../src/client";
import { MatrixEvent } from "../../../src/models/event";
import * as algorithms from "../../../src/crypto/algorithms";
import { WebStorageSessionStore } from "../../../src/store/session/webstorage";
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../../MockStorageApi";
import * as testUtils from "../../test-utils/test-utils";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
import { StubStore } from "../../../src/store/stub";
import { MatrixScheduler } from '../../../src';
const Olm = global.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const ROOM_ID = '!ROOM:ID';
@@ -93,8 +95,8 @@ const AES256_KEY_BACKUP_DATA = {
};
const CURVE25519_BACKUP_INFO = {
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1',
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
@@ -102,7 +104,7 @@ const CURVE25519_BACKUP_INFO = {
const AES256_BACKUP_INFO = {
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: 1,
version: '1',
auth_data: {
// FIXME: add iv and mac
},
@@ -118,30 +120,22 @@ function saveCrossSigningKeys(k) {
Object.assign(keys, k);
}
function makeTestClient(sessionStore, cryptoStore) {
function makeTestClient(cryptoStore) {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
const store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const store = new StubStore();
return new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: function() {}, // NOP
fetchFn: jest.fn(), // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
});
@@ -160,8 +154,6 @@ describe("MegolmBackup", function() {
let olmDevice;
let mockOlmLib;
let mockCrypto;
let mockStorage;
let sessionStore;
let cryptoStore;
let megolmDecryption;
beforeEach(async function() {
@@ -173,9 +165,7 @@ describe("MegolmBackup", function() {
);
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore(mockStorage);
cryptoStore = new MemoryCryptoStore();
olmDevice = new OlmDevice(cryptoStore);
@@ -188,7 +178,6 @@ describe("MegolmBackup", function() {
describe("backup", function() {
let mockBaseApis;
let realSetTimeout;
beforeEach(function() {
mockBaseApis = {};
@@ -206,14 +195,14 @@ describe("MegolmBackup", function() {
// clobber the setTimeout function to run 100x faster.
// ideally we would use lolex, but we have no oportunity
// to tick the clock between the first try and the retry.
realSetTimeout = global.setTimeout;
global.setTimeout = function(f, n) {
return realSetTimeout(f, n/100);
};
const realSetTimeout = global.setTimeout;
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
return realSetTimeout(f!, n/100);
});
});
afterEach(function() {
global.setTimeout = realSetTimeout;
jest.spyOn(global, 'setTimeout').mockRestore();
});
it('automatically calls the key back up', function() {
@@ -225,6 +214,12 @@ describe("MegolmBackup", function() {
const event = new MatrixEvent({
type: 'm.room.encrypted',
});
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
const decryptedData = {
clearEvent: {
type: 'm.room_key',
@@ -261,7 +256,7 @@ describe("MegolmBackup", function() {
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const client = makeTestClient(sessionStore, cryptoStore);
const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
@@ -293,35 +288,35 @@ describe("MegolmBackup", function() {
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
.then(async () => {
await client.enableKeyBackup({
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1',
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect(queryParams.version).toBe('1');
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({});
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -329,6 +324,7 @@ describe("MegolmBackup", function() {
);
}).then(() => {
expect(numCalls).toBe(1);
client.stopClient();
});
});
});
@@ -339,7 +335,7 @@ describe("MegolmBackup", function() {
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const client = makeTestClient(sessionStore, cryptoStore);
const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
@@ -374,36 +370,36 @@ describe("MegolmBackup", function() {
txn);
});
})
.then(() => {
client.enableKeyBackup({
.then(async () => {
await client.enableKeyBackup({
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: 1,
version: '1',
auth_data: {
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect(queryParams.version).toBe('1');
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({});
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -411,6 +407,7 @@ describe("MegolmBackup", function() {
);
}).then(() => {
expect(numCalls).toBe(1);
client.stopClient();
});
});
});
@@ -421,7 +418,7 @@ describe("MegolmBackup", function() {
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const client = makeTestClient(sessionStore, cryptoStore);
const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
@@ -434,22 +431,15 @@ describe("MegolmBackup", function() {
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
let privateKeys;
client.uploadDeviceSigningKeys = async function(e) {return;};
client.uploadKeySignatures = async function(e) {return;};
client.on("crossSigning.saveCrossSigningKeys", function(e) {
privateKeys = e;
});
client.on("crossSigning.getKey", function(e) {
e.done(privateKeys[e.type]);
});
client.uploadDeviceSigningKeys = async function(e) {return {};};
client.uploadKeySignatures = async function(e) {return { failures: {} };};
await resetCrossSigningKeys(client);
let numCalls = 0;
await Promise.all([
new Promise((resolve, reject) => {
new Promise<void>((resolve, reject) => {
let backupInfo;
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
@@ -459,7 +449,7 @@ describe("MegolmBackup", function() {
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
data.auth_data, client.getCrossSigningId(), "@alice:bar",
(data as Record<string, any>).auth_data, client.getCrossSigningId(), "@alice:bar",
);
} catch (e) {
reject(e);
@@ -480,16 +470,17 @@ describe("MegolmBackup", function() {
};
}),
client.createKeyBackupVersion({
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
}),
]);
expect(numCalls).toBe(2);
client.stopClient();
});
it('retries when a backup fails', function() {
it('retries when a backup fails', async function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
@@ -498,26 +489,17 @@ describe("MegolmBackup", function() {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
const store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: function() {}, // NOP
fetchFn: jest.fn(), // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
});
@@ -531,70 +513,68 @@ describe("MegolmBackup", function() {
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
await client.initCrypto();
await cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({});
} else {
return Promise.reject(
new Error("this is an expected failure"),
);
}
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
}).then(() => {
expect(numCalls).toBe(2);
});
txn);
});
await client.enableKeyBackup({
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1',
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
await new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({} as T);
} else {
return Promise.reject(
new Error("this is an expected failure"),
);
}
};
return client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
});
expect(numCalls).toBe(2);
client.stopClient();
});
});
@@ -602,7 +582,7 @@ describe("MegolmBackup", function() {
let client;
beforeEach(function() {
client = makeTestClient(sessionStore, cryptoStore);
client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
@@ -17,12 +17,16 @@ limitations under the License.
import '../../olm-loader';
import anotherjson from 'another-json';
import { PkSigning } from '@matrix-org/olm';
import * as olmlib from "../../../src/crypto/olmlib";
import { TestClient } from '../../TestClient';
import { resetCrossSigningKeys } from "./crypto-utils";
import { MatrixError } from '../../../src/http-api';
import { logger } from '../../../src/logger';
import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client';
import { CryptoEvent } from '../../../src/crypto';
import { IDevice } from '../../../src/crypto/deviceinfo';
import { TestClient } from '../../TestClient';
import { resetCrossSigningKeys } from "./crypto-utils";
const PUSH_RULES_RESPONSE = {
method: "GET",
@@ -47,9 +51,11 @@ function setHttpResponses(httpBackend, responses) {
});
}
async function makeTestClient(userInfo, options, keys) {
if (!keys) keys = {};
async function makeTestClient(
userInfo: { userId: string, deviceId: string},
options: Partial<ICreateClientOpts> = {},
keys = {},
) {
function getCrossSigningKey(type) {
return keys[type];
}
@@ -58,7 +64,6 @@ async function makeTestClient(userInfo, options, keys) {
Object.assign(keys, k);
}
if (!options) options = {};
options.cryptoCallbacks = Object.assign(
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
);
@@ -86,20 +91,21 @@ describe("Cross Signing", function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => {
await olmlib.verifySignature(
alice.crypto.olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
);
});
alice.uploadKeySignatures = async () => {};
alice.setAccountData = async () => {};
alice.getAccountDataFromServer = async () => {};
alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({});
alice.getAccountDataFromServer = async <T>() => ({} as T);
// set Alice's cross-signing key
await alice.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
authUploadDeviceSigningKeys: async func => { await func({}); },
});
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
alice.stopClient();
});
it("should abort bootstrap if device signing auth fails", async function() {
@@ -133,9 +139,9 @@ describe("Cross Signing", function() {
error.httpStatus == 401;
throw error;
};
alice.uploadKeySignatures = async () => {};
alice.setAccountData = async () => {};
alice.getAccountDataFromServer = async () => { };
alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({});
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
const authUploadDeviceSigningKeys = async func => await func({});
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
@@ -151,14 +157,15 @@ describe("Cross Signing", function() {
}
}
expect(bootstrapDidThrow).toBeTruthy();
alice.stopClient();
});
it("should upload a signature when a user is verified", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key
@@ -172,16 +179,20 @@ describe("Cross Signing", function() {
},
},
},
firstUse: false,
crossSigningVerifiedBefore: false,
});
// Alice verifies Bob's key
const promise = new Promise((resolve, reject) => {
alice.uploadKeySignatures = (...args) => {
alice.uploadKeySignatures = async (...args) => {
resolve(...args);
return { failures: {} };
};
});
await alice.setDeviceVerified("@bob:example.com", "bobs+master+pubkey", true);
// Alice should send a signature of Bob's key to the server
await promise;
alice.stopClient();
});
it.skip("should get cross-signing keys from sync", async function() {
@@ -203,7 +214,7 @@ describe("Cross Signing", function() {
{
cryptoCallbacks: {
// will be called to sign our own device
getCrossSigningKey: type => {
getCrossSigningKey: async type => {
if (type === 'master') {
return masterKey;
} else {
@@ -215,7 +226,7 @@ describe("Cross Signing", function() {
);
const keyChangePromise = new Promise((resolve, reject) => {
alice.once("crossSigning.keysChanged", async (e) => {
alice.once(CryptoEvent.KeysChanged, async (e) => {
resolve(e);
await alice.checkOwnCrossSigningTrust({
allowPrivateKeyRequests: true,
@@ -223,14 +234,14 @@ describe("Cross Signing", function() {
});
});
const uploadSigsPromise = new Promise((resolve, reject) => {
alice.uploadKeySignatures = jest.fn(async (content) => {
const uploadSigsPromise = new Promise<void>((resolve, reject) => {
alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => {
try {
await olmlib.verifySignature(
alice.crypto.olmDevice,
content["@alice:example.com"][
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
],
],
"@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
);
@@ -246,16 +257,22 @@ describe("Cross Signing", function() {
});
});
// @ts-ignore private property
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
device_id: "Osborne2",
keys: deviceInfo.keys,
algorithms: deviceInfo.algorithms,
};
aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms;
await alice.crypto.signObject(aliceDevice);
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
olmlib.pkSign(
aliceDevice as ISignedKey,
selfSigningKey as unknown as PkSigning,
"@alice:example.com",
'',
);
// feed sync result that includes master key, ssk, device key
const responses = [
@@ -353,14 +370,15 @@ describe("Cross Signing", function() {
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(aliceDeviceTrust.isTofu()).toBeTruthy();
expect(aliceDeviceTrust.isVerified()).toBeTruthy();
alice.stopClient();
});
it("should use trust chain to determine device verification", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
@@ -370,7 +388,7 @@ describe("Cross Signing", function() {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK = {
const bobSSK: ICrossSigningKey = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -394,10 +412,10 @@ describe("Cross Signing", function() {
},
self_signing: bobSSK,
},
firstUse: 1,
unsigned: {},
firstUse: true,
crossSigningVerifiedBefore: false,
});
const bobDevice = {
const bobDeviceUnsigned = {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
@@ -406,11 +424,16 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey",
},
};
const sig = bobSigning.sign(anotherjson.stringify(bobDevice));
bobDevice.signatures = {
"@bob:example.com": {
["ed25519:" + bobPubkey]: sig,
const sig = bobSigning.sign(anotherjson.stringify(bobDeviceUnsigned));
const bobDevice: IDevice = {
...bobDeviceUnsigned,
signatures: {
"@bob:example.com": {
["ed25519:" + bobPubkey]: sig,
},
},
verified: 0,
known: false,
};
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
@@ -425,7 +448,7 @@ describe("Cross Signing", function() {
expect(bobDeviceTrust.isTofu()).toBeTruthy();
// Alice verifies Bob's SSK
alice.uploadKeySignatures = () => {};
alice.uploadKeySignatures = async () => ({ failures: {} });
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
// Bob's device key should be trusted
@@ -437,10 +460,11 @@ describe("Cross Signing", function() {
expect(bobDeviceTrust2.isCrossSigningVerified()).toBeTruthy();
expect(bobDeviceTrust2.isLocallyVerified()).toBeFalsy();
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
alice.stopClient();
});
it.skip("should trust signatures received from other devices", async function() {
const aliceKeys = {};
const aliceKeys: Record<string, PkSigning> = {};
const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
null,
@@ -448,8 +472,8 @@ describe("Cross Signing", function() {
);
alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com");
alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {};
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
@@ -461,28 +485,29 @@ describe("Cross Signing", function() {
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]);
const keyChangePromise = new Promise((resolve, reject) => {
alice.crypto.deviceList.once("userCrossSigningUpdated", (userId) => {
const keyChangePromise = new Promise<void>((resolve, reject) => {
alice.crypto.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
if (userId === "@bob:example.com") {
resolve();
}
});
});
// @ts-ignore private property
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
device_id: "Osborne2",
keys: deviceInfo.keys,
algorithms: deviceInfo.algorithms,
};
aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms;
await alice.crypto.signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account();
bobOlmAccount.create();
const bobKeys = JSON.parse(bobOlmAccount.identity_keys());
const bobDevice = {
const bobDeviceUnsigned = {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
@@ -491,15 +516,25 @@ describe("Cross Signing", function() {
"curve25519:Dynabook": bobKeys.curve25519,
},
};
const deviceStr = anotherjson.stringify(bobDevice);
bobDevice.signatures = {
"@bob:example.com": {
"ed25519:Dynabook": bobOlmAccount.sign(deviceStr),
const deviceStr = anotherjson.stringify(bobDeviceUnsigned);
const bobDevice: IDevice = {
...bobDeviceUnsigned,
signatures: {
"@bob:example.com": {
"ed25519:Dynabook": bobOlmAccount.sign(deviceStr),
},
},
verified: 0,
known: false,
};
olmlib.pkSign(bobDevice, selfSigningKey, "@bob:example.com");
olmlib.pkSign(
bobDevice,
selfSigningKey as unknown as PkSigning,
"@bob:example.com",
'',
);
const bobMaster = {
const bobMaster: ICrossSigningKey = {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
@@ -507,7 +542,7 @@ describe("Cross Signing", function() {
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
},
};
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com");
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com", '');
// Alice downloads Bob's keys
// - device key
@@ -600,14 +635,15 @@ describe("Cross Signing", function() {
expect(bobDeviceTrust.isCrossSigningVerified()).toBeTruthy();
expect(bobDeviceTrust.isLocallyVerified()).toBeFalsy();
expect(bobDeviceTrust.isTofu()).toBeTruthy();
alice.stopClient();
});
it("should dis-trust an unsigned device", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
@@ -618,7 +654,7 @@ describe("Cross Signing", function() {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK = {
const bobSSK: ICrossSigningKey = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -642,8 +678,8 @@ describe("Cross Signing", function() {
},
self_signing: bobSSK,
},
firstUse: 1,
unsigned: {},
firstUse: true,
crossSigningVerifiedBefore: false,
});
const bobDevice = {
user_id: "@bob:example.com",
@@ -655,7 +691,7 @@ describe("Cross Signing", function() {
},
};
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
Dynabook: bobDevice as unknown as IDevice,
});
// Bob's device key should be untrusted
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
@@ -669,14 +705,15 @@ describe("Cross Signing", function() {
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust2.isVerified()).toBeFalsy();
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
alice.stopClient();
});
it("should dis-trust a user when their ssk changes", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
await resetCrossSigningKeys(alice);
// Alice downloads Bob's keys
const bobMasterSigning = new global.Olm.PkSigning();
@@ -685,7 +722,7 @@ describe("Cross Signing", function() {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK = {
const bobSSK: ICrossSigningKey = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -709,10 +746,10 @@ describe("Cross Signing", function() {
},
self_signing: bobSSK,
},
firstUse: 1,
unsigned: {},
firstUse: true,
crossSigningVerifiedBefore: false,
});
const bobDevice = {
const bobDeviceUnsigned = {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
@@ -721,16 +758,23 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey",
},
};
const bobDeviceString = anotherjson.stringify(bobDevice);
const bobDeviceString = anotherjson.stringify(bobDeviceUnsigned);
const sig = bobSigning.sign(bobDeviceString);
bobDevice.signatures = {};
bobDevice.signatures["@bob:example.com"] = {};
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
const bobDevice: IDevice = {
...bobDeviceUnsigned,
verified: 0,
known: false,
signatures: {
"@bob:example.com": {
["ed25519:" + bobPubkey]: sig,
},
},
};
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Alice verifies Bob's SSK
alice.uploadKeySignatures = () => {};
alice.uploadKeySignatures = async () => ({ failures: {} });
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
// Bob's device key should be trusted
@@ -745,7 +789,7 @@ describe("Cross Signing", function() {
const bobSigning2 = new global.Olm.PkSigning();
const bobPrivkey2 = bobSigning2.generate_seed();
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
const bobSSK2 = {
const bobSSK2: ICrossSigningKey = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -769,8 +813,8 @@ describe("Cross Signing", function() {
},
self_signing: bobSSK2,
},
firstUse: 0,
unsigned: {},
firstUse: false,
crossSigningVerifiedBefore: false,
});
// Bob's and his device should be untrusted
const bobTrust = alice.checkUserTrust("@bob:example.com");
@@ -782,7 +826,7 @@ describe("Cross Signing", function() {
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
// Alice verifies Bob's SSK
alice.uploadKeySignatures = () => {};
alice.uploadKeySignatures = async () => ({ failures: {} });
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true);
// Bob should be trusted but not his device
@@ -805,6 +849,7 @@ describe("Cross Signing", function() {
const bobDeviceTrust4 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust4.isCrossSigningVerified()).toBeTruthy();
alice.stopClient();
});
it("should offer to upgrade device verifications to cross-signing", async function() {
@@ -814,20 +859,21 @@ describe("Cross Signing", function() {
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
shouldUpgradeDeviceVerifications: (verifs) => {
shouldUpgradeDeviceVerifications: async (verifs) => {
expect(verifs.users["@bob:example.com"]).toBeDefined();
upgradeResolveFunc();
return ["@bob:example.com"];
},
},
},
);
const { client: bob } = await makeTestClient(
{ userId: "@bob:example.com", deviceId: "Dynabook" },
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = async () => ({ failures: {} });
// set Bob's cross-signing key
await resetCrossSigningKeys(bob);
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
@@ -846,8 +892,8 @@ describe("Cross Signing", function() {
bob.crypto.crossSigningInfo.toStorage(),
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// when alice sets up cross-signing, she should notice that bob's
// cross-signing key is signed by his Dynabook, which alice has
// verified, and ask if the device verification should be upgraded to a
@@ -873,15 +919,17 @@ describe("Cross Signing", function() {
upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve;
});
alice.crypto.deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
alice.crypto.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
await new Promise((resolve) => {
alice.crypto.on("userTrustStatusChanged", resolve);
alice.crypto.on(CryptoEvent.UserTrustStatusChanged, resolve);
});
await upgradePromise;
const bobTrust3 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust3.isCrossSigningVerified()).toBeTruthy();
expect(bobTrust3.isTofu()).toBeTruthy();
alice.stopClient();
bob.stopClient();
});
it(
@@ -890,8 +938,8 @@ describe("Cross Signing", function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
@@ -900,7 +948,7 @@ describe("Cross Signing", function() {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK = {
const aliceSSK: ICrossSigningKey = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
@@ -926,34 +974,42 @@ describe("Cross Signing", function() {
},
self_signing: aliceSSK,
},
firstUse: 1,
unsigned: {},
firstUse: true,
crossSigningVerifiedBefore: false,
});
// Alice has a second device that's cross-signed
const aliceCrossSignedDevice = {
const aliceDeviceId = 'Dynabook';
const aliceUnsignedDevice = {
user_id: "@alice:example.com",
device_id: "Dynabook",
device_id: aliceDeviceId,
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": "somePubkey",
"ed25519:Dynabook": "someOtherPubkey",
},
};
const sig = aliceSigning.sign(anotherjson.stringify(aliceCrossSignedDevice));
aliceCrossSignedDevice.signatures = {
"@alice:example.com": {
["ed25519:" + alicePubkey]: sig,
},
};
const sig = aliceSigning.sign(anotherjson.stringify(aliceUnsignedDevice));
const aliceCrossSignedDevice: IDevice = {
...aliceUnsignedDevice,
verified: 0,
known: false,
signatures: {
"@alice:example.com": {
["ed25519:" + alicePubkey]: sig,
},
} };
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
Dynabook: aliceCrossSignedDevice,
[aliceDeviceId]: aliceCrossSignedDevice,
});
// We don't trust the cross-signing keys yet...
expect(alice.checkDeviceTrust(aliceCrossSignedDevice.device_id).isCrossSigningVerified()).toBeFalsy();
expect(
alice.checkDeviceTrust("@alice:example.com", aliceDeviceId).isCrossSigningVerified(),
).toBeFalsy();
// ... but we do acknowledge that the device is signed by them
expect(alice.checkIfOwnDeviceCrossSigned(aliceCrossSignedDevice.device_id)).toBeTruthy();
expect(alice.checkIfOwnDeviceCrossSigned(aliceDeviceId)).toBeTruthy();
alice.stopClient();
},
);
@@ -961,8 +1017,8 @@ describe("Cross Signing", function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
@@ -971,7 +1027,7 @@ describe("Cross Signing", function() {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK = {
const aliceSSK: ICrossSigningKey = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
@@ -997,14 +1053,14 @@ describe("Cross Signing", function() {
},
self_signing: aliceSSK,
},
firstUse: 1,
unsigned: {},
firstUse: true,
crossSigningVerifiedBefore: false,
});
// Alice has a second device that's also not cross-signed
const aliceNotCrossSignedDevice = {
user_id: "@alice:example.com",
device_id: "Dynabook",
const deviceId = "Dynabook";
const aliceNotCrossSignedDevice: IDevice = {
verified: 0,
known: false,
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": "somePubkey",
@@ -1012,9 +1068,10 @@ describe("Cross Signing", function() {
},
};
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
Dynabook: aliceNotCrossSignedDevice,
[deviceId]: aliceNotCrossSignedDevice,
});
expect(alice.checkIfOwnDeviceCrossSigned(aliceNotCrossSignedDevice.device_id)).toBeFalsy();
expect(alice.checkIfOwnDeviceCrossSigned(deviceId)).toBeFalsy();
alice.stopClient();
});
});
@@ -1,11 +1,13 @@
import { IRecoveryKey } from '../../../src/crypto/api';
import { CrossSigningLevel } from '../../../src/crypto/CrossSigning';
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
// needs to be phased out and replaced with bootstrapSecretStorage,
// but that is doing too much extra stuff for it to be an easy transition.
export async function resetCrossSigningKeys(client, {
level,
authUploadDeviceSigningKeys = async func => await func(),
} = {}) {
export async function resetCrossSigningKeys(
client,
{ level }: { level?: CrossSigningLevel} = {},
): Promise<void> {
const crypto = client.crypto;
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
@@ -30,14 +32,14 @@ export async function resetCrossSigningKeys(client, {
await crypto.afterCrossSigningLocalKeyChange();
}
export async function createSecretStorageKey() {
export async function createSecretStorageKey(): Promise<IRecoveryKey> {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
decryption.free();
return {
// `pubkey` not used anymore with symmetric 4S
keyInfo: { pubkey: storagePublicKey },
keyInfo: { pubkey: storagePublicKey, key: undefined },
privateKey: storagePrivateKey,
};
}
@@ -1,85 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
const requests = [
{
requestId: "A",
requestBody: { session_id: "A", room_id: "A" },
state: RoomKeyRequestState.Sent,
},
{
requestId: "B",
requestBody: { session_id: "B", room_id: "B" },
state: RoomKeyRequestState.Sent,
},
{
requestId: "C",
requestBody: { session_id: "C", room_id: "C" },
state: RoomKeyRequestState.Unsent,
},
];
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
store._backend = new MemoryCryptoStore();
store._backendPromise = Promise.resolve(store._backend);
return store;
}],
])("Outgoing room key requests [%s]", function(name, dbFactory) {
let store;
beforeAll(async () => {
store = dbFactory();
await store.startup();
await Promise.all(requests.map((request) =>
store.getOrAddOutgoingRoomKeyRequest(request),
));
});
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
async () => {
const r = await
store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
expect(r).toHaveLength(2);
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => {
expect(r).toContainEqual(e);
});
});
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
async () => {
const r =
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
expect(r).not.toBeNull();
expect(r).not.toBeUndefined();
expect(r.state).toEqual(RoomKeyRequestState.Sent);
expect(requests).toContainEqual(r);
});
});
@@ -0,0 +1,99 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { CryptoStore } from '../../../src/crypto/store/base';
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
const requests = [
{
requestId: "A",
requestBody: { session_id: "A", room_id: "A", sender_key: "A", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
},
{
requestId: "B",
requestBody: { session_id: "B", room_id: "B", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@carrie:example.com", deviceId: "barbazquux" },
],
},
{
requestId: "C",
requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Unsent,
recipients: [
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
},
];
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("Outgoing room key requests [%s]", function(name, dbFactory) {
let store: CryptoStore;
beforeAll(async () => {
store = dbFactory();
await store.startup();
await Promise.all(requests.map((request) =>
store.getOrAddOutgoingRoomKeyRequest(request),
));
});
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
async () => {
const r = await
store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
expect(r).toHaveLength(2);
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => {
expect(r).toContainEqual(e);
});
});
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target",
async () => {
const r = await store.getOutgoingRoomKeyRequestsByTarget(
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent],
);
expect(r).toHaveLength(1);
expect(r[0]).toEqual(requests[0]);
});
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
async () => {
const r =
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
expect(r).not.toBeNull();
expect(r).not.toBeUndefined();
expect(r.state).toEqual(RoomKeyRequestState.Sent);
expect(requests).toContainEqual(r);
});
});
@@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,16 +23,11 @@ import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes";
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
import { logger } from '../../../src/logger';
import * as utils from "../../../src/utils";
import { ICreateClientOpts } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support');
}
async function makeTestClient(userInfo, options) {
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
)).client;
@@ -46,7 +41,7 @@ async function makeTestClient(userInfo, options) {
await client.initCrypto();
// No need to download keys for these tests
client.crypto.downloadKeys = async function() {};
jest.spyOn(client.crypto, 'downloadKeys').mockResolvedValue({});
return client;
}
@@ -54,7 +49,7 @@ async function makeTestClient(userInfo, options) {
// Wrapper around pkSign to return a signed object. pkSign returns the
// signature, rather than the signed object.
function sign(obj, key, userId) {
olmlib.pkSign(obj, key, userId);
olmlib.pkSign(obj, key, userId, '');
return obj;
}
@@ -84,7 +79,7 @@ describe("Secrets", function() {
},
};
const getKey = jest.fn(e => {
const getKey = jest.fn().mockImplementation(async e => {
expect(Object.keys(e.keys)).toEqual(["abc"]);
return ['abc', key];
});
@@ -93,7 +88,7 @@ describe("Secrets", function() {
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
getCrossSigningKey: t => signingKey,
getCrossSigningKey: async t => signingKey,
getSecretStorageKey: getKey,
},
},
@@ -104,17 +99,16 @@ describe("Secrets", function() {
const secretStorage = alice.crypto.secretStorage;
alice.setAccountData = async function(eventType, contents, callback) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
if (callback) {
callback();
}
};
jest.spyOn(alice, 'setAccountData').mockImplementation(
async function(eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
return {};
});
const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
@@ -136,6 +130,7 @@ describe("Secrets", function() {
expect(await secretStorage.get("foo")).toBe("bar");
expect(getKey).toHaveBeenCalled();
alice.stopClient();
});
it("should throw if given a key that doesn't exist", async function() {
@@ -150,6 +145,7 @@ describe("Secrets", function() {
expect(true).toBeFalsy();
} catch (e) {
}
alice.stopClient();
});
it("should refuse to encrypt with zero keys", async function() {
@@ -162,12 +158,13 @@ describe("Secrets", function() {
expect(true).toBeFalsy();
} catch (e) {
}
alice.stopClient();
});
it("should encrypt with default key if keys is null", async function() {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn(e => {
const getKey = jest.fn().mockImplementation(async e => {
expect(Object.keys(e.keys)).toEqual([newKeyId]);
return [newKeyId, key];
});
@@ -183,18 +180,19 @@ describe("Secrets", function() {
},
},
);
alice.setAccountData = async function(eventType, contents, callback) {
alice.setAccountData = async function(eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
return {};
};
resetCrossSigningKeys(alice);
const { keyId: newKeyId } = await alice.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
SECRET_STORAGE_ALGORITHM_V1_AES, { pubkey: undefined, key: undefined },
);
// we don't await on this because it waits for the event to come down the sync
// which won't happen in the test setup
@@ -203,6 +201,7 @@ describe("Secrets", function() {
const accountData = alice.getAccountData('foo');
expect(accountData.getContent().encrypted).toBeTruthy();
alice.stopClient();
});
it("should refuse to encrypt if no keys given and no default key", async function() {
@@ -215,10 +214,11 @@ describe("Secrets", function() {
expect(true).toBeFalsy();
} catch (e) {
}
alice.stopClient();
});
it("should request secrets from other clients", async function() {
const [osborne2, vax] = await makeTestClients(
const [[osborne2, vax], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{ userId: "@alice:example.com", deviceId: "VAX" },
@@ -239,20 +239,20 @@ describe("Secrets", function() {
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": {
user_id: "@alice:example.com",
device_id: "VAX",
known: false,
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:VAX": vaxDevice.deviceEd25519Key,
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
},
verified: DeviceInfo.DeviceVerification.VERIFIED,
},
});
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": {
user_id: "@alice:example.com",
device_id: "Osborne2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
verified: 0,
known: false,
keys: {
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
@@ -269,10 +269,15 @@ describe("Secrets", function() {
Object.values(otks)[0],
);
const request = await secretStorage.request("foo", ["VAX"]);
const secret = await request.promise;
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
expect(secret).toBe("bar");
const request = await secretStorage.request("foo", ["VAX"]);
await request.promise; // return value not used
osborne2.stop();
vax.stop();
clearTestClientTimeouts();
});
describe("bootstrap", function() {
@@ -298,7 +303,7 @@ describe("Secrets", function() {
it("bootstraps when no storage or cross-signing keys locally", async function() {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn(e => {
const getKey = jest.fn().mockImplementation(async e => {
return [Object.keys(e.keys)[0], key];
});
@@ -313,9 +318,9 @@ describe("Secrets", function() {
},
},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
bob.setAccountData = async function(eventType, contents) {
const event = new MatrixEvent({
type: eventType,
content: contents,
@@ -324,10 +329,11 @@ describe("Secrets", function() {
event,
]);
this.emit("accountData", event);
return {};
};
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
authUploadDeviceSigningKeys: async func => { await func({}); },
});
await bob.bootstrapSecretStorage({
createSecretStorageKey,
@@ -340,6 +346,7 @@ describe("Secrets", function() {
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
bob.stopClient();
});
it("bootstraps when cross-signing keys in secret storage", async function() {
@@ -406,10 +413,11 @@ describe("Secrets", function() {
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
bob.stopClient();
});
it("adds passphrase checking if it's lacking", async function() {
let crossSigningKeys = {
let crossSigningKeys: Record<string, Uint8Array> = {
master: XSK,
user_signing: USK,
self_signing: SSK,
@@ -421,9 +429,9 @@ describe("Secrets", function() {
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
getCrossSigningKey: t => crossSigningKeys[t],
getCrossSigningKey: async t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getSecretStorageKey: ({ keys }, name) => {
getSecretStorageKey: async ({ keys }, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
return [keyId, secretStorageKeys[keyId]];
@@ -479,6 +487,8 @@ describe("Secrets", function() {
}),
]);
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
firstUse: false,
crossSigningVerifiedBefore: false,
keys: {
master: {
user_id: "@alice:example.com",
@@ -519,14 +529,15 @@ describe("Secrets", function() {
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
return {};
};
await alice.bootstrapSecretStorage();
await alice.bootstrapSecretStorage({});
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
.toEqual({ key: "key_id" });
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
.getContent();
.getContent() as ISecretStorageKeyInfo;
expect(keyInfo.algorithm)
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({
@@ -538,9 +549,10 @@ describe("Secrets", function() {
expect(keyInfo).toHaveProperty("mac");
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
.toBeTruthy();
alice.stopClient();
});
it("fixes backup keys in the wrong format", async function() {
let crossSigningKeys = {
let crossSigningKeys: Record<string, Uint8Array> = {
master: XSK,
user_signing: USK,
self_signing: SSK,
@@ -552,9 +564,9 @@ describe("Secrets", function() {
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
getCrossSigningKey: t => crossSigningKeys[t],
getCrossSigningKey: async t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getSecretStorageKey: ({ keys }, name) => {
getSecretStorageKey: async ({ keys }, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
return [keyId, secretStorageKeys[keyId]];
@@ -619,6 +631,8 @@ describe("Secrets", function() {
}),
]);
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
firstUse: false,
crossSigningVerifiedBefore: false,
keys: {
master: {
user_id: "@alice:example.com",
@@ -659,15 +673,17 @@ describe("Secrets", function() {
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
return {};
};
await alice.bootstrapSecretStorage();
await alice.bootstrapSecretStorage({});
const backupKey = alice.getAccountData("m.megolm_backup.v1")
.getContent();
expect(backupKey.encrypted).toHaveProperty("key_id");
expect(await alice.getSecret("m.megolm_backup.v1"))
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
alice.stopClient();
});
});
});
@@ -13,9 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "../../../../src/client";
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { MatrixEvent } from "../../../../src/models/event";
"../../../../src/crypto/verification/request/ToDeviceChannel";
describe("InRoomChannel tests", function() {
const ALICE = "@alice:hs.tld";
@@ -23,7 +23,7 @@ describe("InRoomChannel tests", function() {
const MALORY = "@malory:hs.tld";
const client = {
getUserId() { return ALICE; },
};
} as unknown as MatrixClient;
it("getEventType only returns .request for a message with a msgtype", function() {
const invalidEvent = new MatrixEvent({
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../../olm-loader";
import { verificationMethods } from "../../../../src/crypto";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import { logger } from "../../../../src/logger";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
@@ -40,7 +40,7 @@ describe("verification request integration tests with crypto layer", function()
});
it("should request and accept a verification", async function() {
const [alice, bob] = await makeTestClients(
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{ userId: "@bob:example.com", deviceId: "Dynabook" },
@@ -52,23 +52,22 @@ describe("verification request integration tests with crypto layer", function()
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
return {
Dynabook: {
algorithms: [],
verified: 0,
known: false,
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
};
};
alice.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.on("crypto.verification.request", (request) => {
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay)
// @ts-ignore Private function access (but it's a test, so we're okay)
bobVerifier.endTimer();
});
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
@@ -76,7 +75,11 @@ describe("verification request integration tests with crypto layer", function()
const aliceVerifier = aliceRequest.verifier;
expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay)
// @ts-ignore Private function access (but it's a test, so we're okay)
aliceVerifier.endTimer();
alice.stop();
bob.stop();
clearTestClientTimeouts();
});
});
@@ -19,10 +19,14 @@ import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
import { MatrixEvent } from "../../../../src/models/event";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { verificationMethods } from "../../../../src/crypto";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib";
import { logger } from "../../../../src/logger";
import { resetCrossSigningKeys } from "../crypto-utils";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { MatrixClient } from "../../../../src";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
const Olm = global.Olm;
@@ -48,13 +52,15 @@ describe("SAS verification", function() {
//channel, baseApis, userId, deviceId, startEvent, request
const request = {
onVerifierCancelled: function() {},
};
} as VerificationRequest;
const channel = {
send: function() {
return Promise.resolve();
},
};
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
} as unknown as IVerificationChannel;
const mockClient = {} as unknown as MatrixClient;
const event = new MatrixEvent({ type: 'test' });
const sas = new SAS(channel, mockClient, "@alice:example.com", "ABCDEFG", event, request);
sas.handleEvent(new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
@@ -65,7 +71,7 @@ describe("SAS verification", function() {
expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel();
sas.cancel(new Error('error'));
});
describe("verification", () => {
@@ -75,9 +81,10 @@ describe("SAS verification", function() {
let bobSasEvent;
let aliceVerifier;
let bobPromise;
let clearTestClientTimeouts;
beforeEach(async () => {
[alice, bob] = await makeTestClients(
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{ userId: "@bob:example.com", deviceId: "Dynabook" },
@@ -178,6 +185,8 @@ describe("SAS verification", function() {
alice.stop(),
bob.stop(),
]);
clearTestClientTimeouts();
});
it("should verify a key", async () => {
@@ -215,7 +224,7 @@ describe("SAS verification", function() {
]);
// make sure that it uses the preferred method
expect(macMethod).toBe("hkdf-hmac-sha256");
expect(macMethod).toBe("org.matrix.msc3783.hkdf-hmac-sha256");
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
// make sure Alice and Bob verified each other
@@ -227,6 +236,62 @@ describe("SAS verification", function() {
expect(aliceDevice.isVerified()).toBeTruthy();
});
it("should be able to verify using the old base64", async () => {
// pretend that Alice can only understand the old (incorrect) base64
// encoding, and make sure that she can still verify with Bob
let macMethod;
const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client);
alice.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.start") {
// Note: this modifies not only the message that Bob
// receives, but also the copy of the message that Alice
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.client.getUserId()][bob.client.deviceId]
.message_authentication_codes = ['hkdf-hmac-sha256'];
}
return aliceOrigSendToDevice(type, map);
};
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
.message_authentication_code;
}
return bobOrigSendToDevice(type, map);
};
alice.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
},
});
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(),
bob.httpBackend.flush(),
]);
expect(macMethod).toBe("hkdf-hmac-sha256");
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy();
});
it("should be able to verify using the old MAC", async () => {
// pretend that Alice can only understand the old (incorrect) MAC,
// and make sure that she can still verify with Bob
@@ -334,7 +399,7 @@ describe("SAS verification", function() {
});
it("should send a cancellation message on error", async function() {
const [alice, bob] = await makeTestClients(
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{ userId: "@bob:example.com", deviceId: "Dynabook" },
@@ -344,16 +409,12 @@ describe("SAS verification", function() {
},
);
alice.client.setDeviceVerified = jest.fn();
alice.client.downloadKeys = () => {
return Promise.resolve();
};
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.setDeviceVerified = jest.fn();
bob.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
const bobPromise = new Promise((resolve, reject) => {
bob.client.on("crypto.verification.request", request => {
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier.on("show_sas", (e) => {
e.mismatch();
});
@@ -362,7 +423,7 @@ describe("SAS verification", function() {
});
const aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId,
verificationMethods.SAS, bob.client.getUserId()!, bob.client.deviceId!,
);
const aliceSpy = jest.fn();
@@ -377,6 +438,10 @@ describe("SAS verification", function() {
.not.toHaveBeenCalled();
expect(bob.client.setDeviceVerified)
.not.toHaveBeenCalled();
alice.stop();
bob.stop();
clearTestClientTimeouts();
});
describe("verification in DM", function() {
@@ -386,9 +451,10 @@ describe("SAS verification", function() {
let bobSasEvent;
let aliceVerifier;
let bobPromise;
let clearTestClientTimeouts;
beforeEach(async function() {
[alice, bob] = await makeTestClients(
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{ userId: "@bob:example.com", deviceId: "Dynabook" },
@@ -398,7 +464,7 @@ describe("SAS verification", function() {
},
);
alice.client.setDeviceVerified = jest.fn();
alice.client.crypto.setDeviceVerification = jest.fn();
alice.client.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
@@ -416,7 +482,7 @@ describe("SAS verification", function() {
return Promise.resolve();
};
bob.client.setDeviceVerified = jest.fn();
bob.client.crypto.setDeviceVerification = jest.fn();
bob.client.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
@@ -437,7 +503,7 @@ describe("SAS verification", function() {
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bobPromise = new Promise<void>((resolve, reject) => {
bob.client.on("crypto.verification.request", async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME);
verifier.on("show_sas", (e) => {
@@ -488,6 +554,8 @@ describe("SAS verification", function() {
alice.stop(),
bob.stop(),
]);
clearTestClientTimeouts();
});
it("should verify a key", async function() {
@@ -497,10 +565,24 @@ describe("SAS verification", function() {
]);
// make sure Alice and Bob verified each other
expect(alice.client.setDeviceVerified)
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
expect(bob.client.setDeviceVerified)
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
expect(alice.client.crypto.setDeviceVerification)
.toHaveBeenCalledWith(
bob.client.getUserId(),
bob.client.deviceId,
true,
null,
null,
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
);
expect(bob.client.crypto.setDeviceVerification)
.toHaveBeenCalledWith(
alice.client.getUserId(),
alice.client.deviceId,
true,
null,
null,
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
);
});
});
});
@@ -18,6 +18,9 @@ import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
import { encodeBase64 } from "../../../../src/crypto/olmlib";
import { setupWebcrypto, teardownWebcrypto } from './util';
import { VerificationBase } from '../../../../src/crypto/verification/Base';
import { MatrixClient, MatrixEvent } from '../../../../src';
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
jest.useFakeTimers();
@@ -54,9 +57,21 @@ describe("self-verifications", () => {
cacheCallbacks,
);
crossSigningInfo.keys = {
master: { keys: { X: testKeyPub } },
self_signing: { keys: { X: testKeyPub } },
user_signing: { keys: { X: testKeyPub } },
master: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
self_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
user_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
};
const secretStorage = {
@@ -79,20 +94,22 @@ describe("self-verifications", () => {
getUserId: () => userId,
getKeyBackupVersion: () => Promise.resolve({}),
restoreKeyBackupWithCache,
};
} as unknown as MatrixClient;
const request = {
onVerifierFinished: () => undefined,
};
} as unknown as VerificationRequest;
const verification = new VerificationBase(
undefined, // channel
undefined as unknown as IVerificationChannel, // channel
client, // baseApis
userId,
"ABC", // deviceId
undefined, // startEvent
undefined as unknown as MatrixEvent, // startEvent
request,
);
// @ts-ignore set private property
verification.resolve = () => undefined;
const result = await verification.done();
@@ -102,12 +119,12 @@ describe("self-verifications", () => {
expect(secretStorage.request.mock.calls.length).toBe(4);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
.toEqual(testKey);
.toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
.toEqual(testKey);
.toEqual(testKey);
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
.toEqual(testKey);
.toEqual(testKey);
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
@@ -19,19 +19,23 @@ import nodeCrypto from "crypto";
import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
import { Room, RoomEvent } from "../../../../src/models/room";
import { logger } from '../../../../src/logger';
import { MatrixClient, ClientEvent } from '../../../../src/client';
export async function makeTestClients(userInfos, options) {
const clients = [];
const clientMap = {};
const sendToDevice = function(type, map) {
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
const clients: TestClient[] = [];
const timeouts: ReturnType<typeof setTimeout>[] = [];
const clientMap: Record<string, Record<string, MatrixClient>> = {};
const makeSendToDevice = (matrixClient: MatrixClient): MatrixClient['sendToDevice'] => async (type, map) => {
// logger.log(this.getUserId(), "sends", type, map);
for (const [userId, devMap] of Object.entries(map)) {
if (userId in clientMap) {
for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
sender: matrixClient.getUserId()!,
type: type,
content: msg,
});
@@ -41,18 +45,19 @@ export async function makeTestClients(userInfos, options) {
Promise.resolve();
decryptionPromise.then(
() => client.emit("toDeviceEvent", event),
() => client.emit(ClientEvent.ToDeviceEvent, event),
);
}
}
}
}
return {};
};
const sendEvent = function(room, type, content) {
const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this
const eventId = "$" + matrixClient.makeTxnId();
const rawEvent = {
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
sender: matrixClient.getUserId()!,
type: type,
content: content,
room_id: room,
@@ -62,21 +67,25 @@ export async function makeTestClients(userInfos, options) {
const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
unsigned: {
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this
transaction_id: matrixClient.makeTxnId(),
},
}));
setImmediate(() => {
const timeout = setTimeout(() => {
for (const tc of clients) {
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
const room = new Room('test', tc.client, tc.client.getUserId()!);
const roomTimelineData = {} as unknown as IRoomTimelineData;
if (tc.client === matrixClient) {
logger.log("sending remote echo!!");
tc.client.emit("Room.timeline", remoteEcho);
tc.client.emit(RoomEvent.Timeline, remoteEcho, room, false, false, roomTimelineData);
} else {
tc.client.emit("Room.timeline", event);
tc.client.emit(RoomEvent.Timeline, event, room, false, false, roomTimelineData);
}
}
});
timeouts.push(timeout as unknown as ReturnType<typeof setTimeout>);
return Promise.resolve({ event_id: eventId });
};
@@ -96,24 +105,29 @@ export async function makeTestClients(userInfos, options) {
clientMap[userInfo.userId] = {};
}
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
testClient.client.sendToDevice = sendToDevice;
testClient.client.sendEvent = sendEvent;
testClient.client.sendToDevice = makeSendToDevice(testClient.client);
testClient.client.sendEvent = makeSendEvent(testClient.client);
clients.push(testClient);
}
await Promise.all(clients.map((testClient) => testClient.client.initCrypto()));
return clients;
const destroy = () => {
timeouts.forEach((t) => clearTimeout(t));
};
return [clients, destroy];
}
export function setupWebcrypto() {
global.crypto = {
getRandomValues: (buf) => {
return nodeCrypto.randomFillSync(buf);
return nodeCrypto.randomFillSync(buf as any);
},
};
} as unknown as Crypto;
}
export function teardownWebcrypto() {
// @ts-ignore undefined != Crypto
global.crypto = undefined;
}
@@ -19,11 +19,18 @@ import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoo
import { ToDeviceChannel } from
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client";
import { setupWebcrypto, teardownWebcrypto } from "./util";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
function makeMockClient(userId, deviceId) {
type MockClient = MatrixClient & {
popEvents: () => MatrixEvent[];
popDeviceEvents: (userId: string, deviceId: string) => MatrixEvent[];
};
function makeMockClient(userId: string, deviceId: string): MockClient {
let counter = 1;
let events = [];
let events: MatrixEvent[] = [];
const deviceEvents = {};
return {
getUserId() { return userId; },
@@ -54,16 +61,18 @@ function makeMockClient(userId, deviceId) {
deviceEvents[userId][deviceId].push(event);
}
}
return Promise.resolve();
return Promise.resolve({});
},
popEvents() {
// @ts-ignore special testing fn
popEvents(): MatrixEvent[] {
const e = events;
events = [];
return e;
},
popDeviceEvents(userId, deviceId) {
// @ts-ignore special testing fn
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
const forDevice = deviceEvents[userId];
const events = forDevice && forDevice[deviceId];
const result = events || [];
@@ -72,12 +81,21 @@ function makeMockClient(userId, deviceId) {
}
return result;
},
};
} as unknown as MockClient;
}
const MOCK_METHOD = "mock-verify";
class MockVerifier {
constructor(channel, client, userId, deviceId, startEvent) {
class MockVerifier extends VerificationBase<'', any> {
public _channel;
public _startEvent;
constructor(
channel: IVerificationChannel,
client: MatrixClient,
userId: string,
deviceId: string,
startEvent: MatrixEvent,
) {
super(channel, client, userId, deviceId, startEvent, {} as unknown as VerificationRequest);
this._channel = channel;
this._startEvent = startEvent;
}
@@ -115,7 +133,10 @@ function makeRemoteEcho(event) {
async function distributeEvent(ownRequest, theirRequest, event) {
await ownRequest.channel.handleEvent(
makeRemoteEcho(event), ownRequest, true);
makeRemoteEcho(event),
ownRequest,
true,
);
await theirRequest.channel.handleEvent(event, theirRequest, true);
}
@@ -133,12 +154,19 @@ describe("verification request unit tests", function() {
it("transition from UNSENT to DONE through happy path", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const verificationMethods = new Map(
[[MOCK_METHOD, MockVerifier]],
) as unknown as Map<string, typeof VerificationBase>;
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([[MOCK_METHOD, MockVerifier]]), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
verificationMethods,
alice,
);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([[MOCK_METHOD, MockVerifier]]), bob);
verificationMethods,
bob,
);
expect(aliceRequest.invalid).toBe(true);
expect(bobRequest.invalid).toBe(true);
@@ -157,7 +185,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest.ready).toBe(true);
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
await verifier.start();
await (verifier as MockVerifier).start();
const [startEvent] = alice.popEvents();
expect(startEvent.getType()).toBe(START_TYPE);
await distributeEvent(aliceRequest, bobRequest, startEvent);
@@ -165,8 +193,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
expect(bobRequest.started).toBe(true);
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
await bobRequest.verifier.start();
await (bobRequest.verifier as MockVerifier).start();
const [bobDoneEvent] = bob.popEvents();
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
@@ -180,12 +207,20 @@ describe("verification request unit tests", function() {
it("methods only contains common methods", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceVerificationMethods = new Map(
[["c", function() {}], ["a", function() {}]],
) as unknown as Map<string, typeof VerificationBase>;
const bobVerificationMethods = new Map(
[["c", function() {}], ["b", function() {}]],
) as unknown as Map<string, typeof VerificationBase>;
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([["c", function() {}], ["a", function() {}]]), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
aliceVerificationMethods, alice);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([["c", function() {}], ["b", function() {}]]), bob);
bobVerificationMethods,
bob,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await distributeEvent(aliceRequest, bobRequest, requestEvent);
@@ -201,13 +236,22 @@ describe("verification request unit tests", function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice);
new InRoomChannel(alice, "!room", bob1.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bob1Request = new VerificationRequest(
new InRoomChannel(bob1, "!room"), new Map(), bob1);
new InRoomChannel(bob1, "!room"),
new Map(),
bob1,
);
const bob2Request = new VerificationRequest(
new InRoomChannel(bob2, "!room"), new Map(), bob2);
new InRoomChannel(bob2, "!room"),
new Map(),
bob2,
);
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
@@ -222,22 +266,34 @@ describe("verification request unit tests", function() {
it("verify own device with to_device messages", async function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const verificationMethods = new Map(
[[MOCK_METHOD, MockVerifier]],
) as unknown as Map<string, typeof VerificationBase>;
const bob1Request = new VerificationRequest(
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
ToDeviceChannel.makeTransactionId(), "device2"),
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
new ToDeviceChannel(
bob1,
bob1.getUserId()!,
["device1", "device2"],
ToDeviceChannel.makeTransactionId(),
"device2",
),
verificationMethods,
bob1,
);
const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
expect(verifier).toBeInstanceOf(MockVerifier);
await verifier.start();
await (verifier as MockVerifier).start();
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
expect(startEvent.getType()).toBe(START_TYPE);
const bob2Request = new VerificationRequest(
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
new ToDeviceChannel(bob2, bob2.getUserId()!, ["device1"]),
verificationMethods,
bob2,
);
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
await bob2Request.verifier.start();
await (bob2Request.verifier as MockVerifier).start();
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
expect(doneEvent1.getType()).toBe(DONE_TYPE);
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
@@ -253,11 +309,13 @@ describe("verification request unit tests", function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
true, true);
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true);
expect(aliceRequest.cancelled).toBe(false);
expect(aliceRequest._cancellingUserId).toBe(undefined);
@@ -269,11 +327,17 @@ describe("verification request unit tests", function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"), new Map(), bob);
new InRoomChannel(bob, "!room"),
new Map(),
bob,
);
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
+5 -1
View File
@@ -29,7 +29,7 @@ describe("eventMapperFor", function() {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: function() {} as any, // NOP
fetchFn: function() {} as any, // NOP
store: {
getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId);
@@ -44,6 +44,10 @@ describe("eventMapperFor", function() {
rooms = [];
});
afterEach(() => {
client.stopClient();
});
it("should de-duplicate MatrixEvent instances by means of findEventById on the room object", async () => {
const roomId = "!room:example.org";
const room = new Room(roomId, client, userId);
+325
View File
@@ -0,0 +1,325 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as utils from "../test-utils/test-utils";
import {
DuplicateStrategy,
EventTimeline,
EventTimelineSet,
EventType,
Filter,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
Room,
} from '../../src';
import { Thread } from "../../src/models/thread";
import { ReEmitter } from "../../src/ReEmitter";
describe('EventTimelineSet', () => {
const roomId = '!foo:bar';
const userA = "@alice:bar";
let room: Room;
let eventTimeline: EventTimeline;
let eventTimelineSet: EventTimelineSet;
let client: MatrixClient;
let messageEvent: MatrixEvent;
let replyEvent: MatrixEvent;
const itShouldReturnTheRelatedEvents = () => {
it('should return the related events', () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId(),
"m.in_reply_to",
EventType.RoomMessage,
);
expect(relations).toBeDefined();
expect(relations.getRelations().length).toBe(1);
expect(relations.getRelations()[0].getId()).toBe(replyEvent.getId());
});
};
beforeEach(() => {
client = utils.mock(MatrixClient, 'MatrixClient');
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter');
room = new Room(roomId, client, userA);
eventTimelineSet = new EventTimelineSet(room);
eventTimeline = new EventTimeline(eventTimelineSet);
messageEvent = utils.mkMessage({
room: roomId,
user: userA,
msg: 'Hi!',
event: true,
});
replyEvent = utils.mkReplyMessage({
room: roomId,
user: userA,
msg: 'Hoo!',
event: true,
replyToMessage: messageEvent,
});
});
describe('addLiveEvent', () => {
it("Adds event to the live timeline in the timeline set", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent);
expect(liveTimeline.getEvents().length).toStrictEqual(1);
});
it("should replace a timeline event if dupe strategy is 'replace'", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent, {
duplicateStrategy: DuplicateStrategy.Replace,
});
expect(liveTimeline.getEvents().length).toStrictEqual(1);
// make a duplicate
const duplicateMessageEvent = utils.mkMessage({
room: roomId, user: userA, msg: "dupe", event: true,
});
duplicateMessageEvent.event.event_id = messageEvent.getId();
// Adding the duplicate event should replace the `messageEvent`
// because it has the same `event_id` and duplicate strategy is
// replace.
eventTimelineSet.addLiveEvent(duplicateMessageEvent, {
duplicateStrategy: DuplicateStrategy.Replace,
});
const eventsInLiveTimeline = liveTimeline.getEvents();
expect(eventsInLiveTimeline.length).toStrictEqual(1);
expect(eventsInLiveTimeline[0]).toStrictEqual(duplicateMessageEvent);
});
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Replace, false)).not.toThrow();
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Ignore, true)).not.toThrow();
});
});
describe('addEventToTimeline', () => {
it("Adds event to timeline", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents().length).toStrictEqual(1);
});
it("Make sure legacy overload passing options directly as parameters still works", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(() => {
eventTimelineSet.addEventToTimeline(
messageEvent,
liveTimeline,
true,
);
}).not.toThrow();
expect(() => {
eventTimelineSet.addEventToTimeline(
messageEvent,
liveTimeline,
true,
false,
);
}).not.toThrow();
});
});
describe('aggregateRelations', () => {
describe('with unencrypted events', () => {
beforeEach(() => {
eventTimelineSet.addEventsToTimeline(
[
messageEvent,
replyEvent,
],
true,
eventTimeline,
'foo',
);
});
itShouldReturnTheRelatedEvents();
});
describe('with events to be decrypted', () => {
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance;
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
beforeEach(() => {
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, 'shouldAttemptDecryption');
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, 'shouldAttemptDecryption');
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
eventTimelineSet.addEventsToTimeline(
[
messageEvent,
replyEvent,
],
true,
eventTimeline,
'foo',
);
});
it('should not return the related events', () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId(),
"m.in_reply_to",
EventType.RoomMessage,
);
expect(relations).toBeUndefined();
});
describe('after decryption', () => {
beforeEach(() => {
// simulate decryption failure once
messageEventIsDecryptionFailureSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy.mockReturnValue(true);
messageEvent.emit(MatrixEventEvent.Decrypted, messageEvent);
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent);
// simulate decryption
messageEventIsDecryptionFailureSpy.mockReturnValue(false);
replyEventIsDecryptionFailureSpy.mockReturnValue(false);
messageEventShouldAttemptDecryptionSpy.mockReturnValue(false);
replyEventShouldAttemptDecryptionSpy.mockReturnValue(false);
messageEvent.emit(MatrixEventEvent.Decrypted, messageEvent);
replyEvent.emit(MatrixEventEvent.Decrypted, replyEvent);
});
itShouldReturnTheRelatedEvents();
});
});
});
describe("canContain", () => {
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}, room.client);
let thread: Thread;
beforeEach(() => {
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
it("should throw if timeline set has no room", () => {
const eventTimelineSet = new EventTimelineSet(undefined, {}, client);
expect(() => eventTimelineSet.canContain(messageEvent)).toThrowError();
});
it("should return false if timeline set is for thread but event is not threaded", () => {
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
expect(eventTimelineSet.canContain(replyEvent)).toBeFalsy();
});
it("should return false if timeline set it for thread but event it for a different thread", () => {
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
const event = mkThreadResponse(replyEvent);
expect(eventTimelineSet.canContain(event)).toBeFalsy();
});
it("should return false if timeline set is not for a thread but event is a thread response", () => {
const eventTimelineSet = new EventTimelineSet(room, {}, client);
const event = mkThreadResponse(replyEvent);
expect(eventTimelineSet.canContain(event)).toBeFalsy();
});
it("should return true if the timeline set is not for a thread and the event is a thread root", () => {
const eventTimelineSet = new EventTimelineSet(room, {}, client);
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
});
it("should return true if the timeline set is for a thread and the event is its thread root", () => {
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
messageEvent.setThread(thread);
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
});
it("should return true if the timeline set is for a thread and the event is a response to it", () => {
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
messageEvent.setThread(thread);
const event = mkThreadResponse(messageEvent);
expect(eventTimelineSet.canContain(event)).toBeTruthy();
});
});
describe("handleRemoteEcho", () => {
it("should add to liveTimeline only if the event matches the filter", () => {
const filter = new Filter(client.getUserId()!, "test_filter");
filter.setDefinition({
room: {
timeline: {
types: [EventType.RoomMessage],
},
},
});
const eventTimelineSet = new EventTimelineSet(room, { filter }, client);
const roomMessageEvent = new MatrixEvent({
type: EventType.RoomMessage,
content: { body: "test" },
event_id: "!test1:server",
});
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
const roomFilteredEvent = new MatrixEvent({
type: "other_event_type",
content: { body: "test" },
event_id: "!test2:server",
});
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
});
});
});
@@ -1,26 +1,36 @@
import { mocked } from 'jest-mock';
import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
import { MatrixClient } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { RoomMember } from "../../src/models/room-member";
import { EventTimelineSet } from "../../src/models/event-timeline-set";
function mockRoomStates(timeline) {
timeline.startState = utils.mock(RoomState, "startState");
timeline.endState = utils.mock(RoomState, "endState");
}
jest.mock("../../src/models/room-state");
describe("EventTimeline", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
let timeline;
let timeline: EventTimeline;
const mockClient = {} as unknown as MatrixClient;
const getTimeline = (): EventTimeline => {
const room = new Room(roomId, mockClient, userA);
const timelineSet = new EventTimelineSet(room);
jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
return new EventTimeline(timelineSet);
};
beforeEach(function() {
// XXX: this is a horrid hack; should use sinon or something instead to mock
const timelineSet = { room: { roomId: roomId } };
timelineSet.room.getUnfilteredTimelineSet = function() {
return timelineSet;
};
// reset any RoomState mocks
jest.resetAllMocks();
timeline = new EventTimeline(timelineSet);
timeline = getTimeline();
});
describe("construction", function() {
@@ -31,10 +41,6 @@ describe("EventTimeline", function() {
});
describe("initialiseState", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
it("should copy state events to start and end state", function() {
const events = [
utils.mkMembership({
@@ -48,11 +54,17 @@ describe("EventTimeline", function() {
}),
];
timeline.initialiseState(events);
expect(timeline.startState.setStateEvents).toHaveBeenCalledWith(
// @ts-ignore private prop
const timelineStartState = timeline.startState;
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(
events,
{ timelineWasEmpty: undefined },
);
expect(timeline.endState.setStateEvents).toHaveBeenCalledWith(
// @ts-ignore private prop
const timelineEndState = timeline.endState;
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(
events,
{ timelineWasEmpty: undefined },
);
});
@@ -73,7 +85,7 @@ describe("EventTimeline", function() {
expect(function() {
timeline.initialiseState(state);
}).not.toThrow();
timeline.addEvent(event, false);
timeline.addEvent(event, { toStartOfTimeline: false });
expect(function() {
timeline.initialiseState(state);
}).toThrow();
@@ -101,8 +113,8 @@ describe("EventTimeline", function() {
});
it("setNeighbouringTimeline should set neighbour", function() {
const prev = { a: "a" };
const next = { b: "b" };
const prev = getTimeline();
const next = getTimeline();
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
@@ -110,8 +122,8 @@ describe("EventTimeline", function() {
});
it("setNeighbouringTimeline should throw if called twice", function() {
const prev = { a: "a" };
const next = { b: "b" };
const prev = getTimeline();
const next = getTimeline();
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow();
@@ -133,10 +145,6 @@ describe("EventTimeline", function() {
});
describe("addEvent", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
@@ -149,9 +157,9 @@ describe("EventTimeline", function() {
];
it("should be able to add events to the end", function() {
timeline.addEvent(events[0], false);
timeline.addEvent(events[0], { toStartOfTimeline: false });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[0]);
@@ -159,9 +167,9 @@ describe("EventTimeline", function() {
});
it("should be able to add events to the start", function() {
timeline.addEvent(events[0], true);
timeline.addEvent(events[0], { toStartOfTimeline: true });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], true);
timeline.addEvent(events[1], { toStartOfTimeline: true });
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[1]);
@@ -169,24 +177,22 @@ describe("EventTimeline", function() {
});
it("should set event.sender for new and old events", function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
const oldSentinel = new RoomMember(roomId, userA);
sentinel.name = "Old Alice";
sentinel.membership = "join";
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
@@ -203,50 +209,48 @@ describe("EventTimeline", function() {
content: { name: "Old Room Name" },
});
timeline.addEvent(newEv, false);
timeline.addEvent(newEv, { toStartOfTimeline: false });
expect(newEv.sender).toEqual(sentinel);
timeline.addEvent(oldEv, true);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
function() {
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
const oldSentinel = new RoomMember(roomId, userA);
sentinel.name = "Old Alice";
sentinel.membership = "join";
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
});
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
});
timeline.addEvent(newEv, { toStartOfTimeline: false });
expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
expect(oldEv.target).toEqual(oldSentinel);
});
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
});
timeline.addEvent(newEv, false);
expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, true);
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
@@ -262,13 +266,13 @@ describe("EventTimeline", function() {
}),
];
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], false);
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
toHaveBeenCalledWith([events[0]]);
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
toHaveBeenCalledWith([events[1]]);
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
@@ -291,13 +295,13 @@ describe("EventTimeline", function() {
}),
];
timeline.addEvent(events[0], true);
timeline.addEvent(events[1], true);
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true });
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
toHaveBeenCalledWith([events[0]]);
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
toHaveBeenCalledWith([events[1]]);
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
@@ -305,6 +309,15 @@ describe("EventTimeline", function() {
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
not.toHaveBeenCalled();
});
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
// @ts-ignore stateContext is not a valid param
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
expect(() => timeline.addEvent(events[0],
{ toStartOfTimeline: false, roomState: new RoomState(roomId) },
)).not.toThrow();
});
});
describe("removeEvent", function() {
@@ -324,8 +337,8 @@ describe("EventTimeline", function() {
];
it("should remove events", function() {
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], false);
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getEvents().length).toEqual(2);
let ev = timeline.removeEvent(events[0].getId());
@@ -338,9 +351,9 @@ describe("EventTimeline", function() {
});
it("should update baseIndex", function() {
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], true);
timeline.addEvent(events[2], false);
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[2], { toStartOfTimeline: false });
expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1);
@@ -357,14 +370,14 @@ describe("EventTimeline", function() {
// - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event",
function() {
timeline.addEvent(events[0], true);
timeline.removeEvent(events[0].getId());
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
timeline.addEvent(events[2], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
function() {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.removeEvent(events[0].getId());
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[2], { toStartOfTimeline: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
});
});
@@ -1,6 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,16 +15,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../../src/logger";
import { MatrixEvent } from "../../src/models/event";
describe("MatrixEvent", () => {
describe(".attemptDecryption", () => {
let encryptedEvent;
const eventId = 'test_encrypted_event';
beforeEach(() => {
encryptedEvent = new MatrixEvent({
id: 'test_encrypted_event',
event_id: eventId,
type: 'm.room.encrypted',
content: {
ciphertext: 'secrets',
@@ -32,45 +32,34 @@ describe("MatrixEvent", () => {
});
});
it('should retry decryption if a retry is queued', () => {
let callCount = 0;
let prom2;
let prom2Fulfilled = false;
it('should retry decryption if a retry is queued', async () => {
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption');
const crypto = {
decryptEvent: function() {
++callCount;
logger.log(`decrypt: ${callCount}`);
if (callCount == 1) {
decryptEvent: jest.fn()
.mockImplementationOnce(() => {
// schedule a second decryption attempt while
// the first one is still running.
prom2 = encryptedEvent.attemptDecryption(crypto);
prom2.then(() => prom2Fulfilled = true);
encryptedEvent.attemptDecryption(crypto);
const error = new Error("nope");
error.name = 'DecryptionError';
return Promise.reject(error);
} else {
expect(prom2Fulfilled).toBe(
false, 'second attemptDecryption resolved too soon');
})
.mockImplementationOnce(() => {
return Promise.resolve({
clearEvent: {
type: 'm.room.message',
},
});
}
},
}),
};
return encryptedEvent.attemptDecryption(crypto).then(() => {
expect(callCount).toEqual(2);
expect(encryptedEvent.getType()).toEqual('m.room.message');
await encryptedEvent.attemptDecryption(crypto);
// make sure the second attemptDecryption resolves
return prom2;
});
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
expect(encryptedEvent.getType()).toEqual('m.room.message');
});
});
});
+62
View File
@@ -0,0 +1,62 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { buildFeatureSupportMap, Feature, ServerSupport } from "../../src/feature";
describe("Feature detection", () => {
it("checks the matrix version", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.3"],
unstable_features: {},
});
expect(support.get(Feature.Thread)).toBe(ServerSupport.Stable);
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
});
it("checks the matrix msc number", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.2"],
unstable_features: {
"org.matrix.msc3771": true,
"org.matrix.msc3773": true,
},
});
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unstable);
});
it("requires two MSCs to pass", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.2"],
unstable_features: {
"org.matrix.msc3771": false,
"org.matrix.msc3773": true,
},
});
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
});
it("requires two MSCs OR matrix versions to pass", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.4"],
unstable_features: {
"org.matrix.msc3771": false,
"org.matrix.msc3773": true,
},
});
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Stable);
});
});
+9 -12
View File
@@ -1,7 +1,4 @@
import {
MatrixEvent,
RelationType,
} from "../../src";
import { RelationType } from "../../src";
import { FilterComponent } from "../../src/filter-component";
import { mkEvent } from '../test-utils/test-utils';
@@ -14,7 +11,7 @@ describe("Filter Component", function() {
content: { },
room: 'roomId',
event: true,
}) as MatrixEvent;
});
const checkResult = filter.check(event);
@@ -28,7 +25,7 @@ describe("Filter Component", function() {
content: { },
room: 'roomId',
event: true,
}) as MatrixEvent;
});
const checkResult = filter.check(event);
@@ -55,7 +52,7 @@ describe("Filter Component", function() {
},
},
},
}) as MatrixEvent;
});
expect(filter.check(threadRootNotParticipated)).toBe(false);
});
@@ -80,7 +77,7 @@ describe("Filter Component", function() {
user: '@someone-else:server.org',
room: 'roomId',
event: true,
}) as MatrixEvent;
});
expect(filter.check(threadRootParticipated)).toBe(true);
});
@@ -100,7 +97,7 @@ describe("Filter Component", function() {
[RelationType.Reference]: {},
},
},
}) as MatrixEvent;
});
expect(filter.check(referenceRelationEvent)).toBe(false);
});
@@ -123,7 +120,7 @@ describe("Filter Component", function() {
},
room: 'roomId',
event: true,
}) as MatrixEvent;
});
const eventWithMultipleRelations = mkEvent({
"type": "m.room.message",
@@ -148,7 +145,7 @@ describe("Filter Component", function() {
},
"room": 'roomId',
"event": true,
}) as MatrixEvent;
});
const noMatchEvent = mkEvent({
"type": "m.room.message",
@@ -160,7 +157,7 @@ describe("Filter Component", function() {
},
"room": 'roomId',
"event": true,
}) as MatrixEvent;
});
expect(filter.check(threadRootEvent)).toBe(true);
expect(filter.check(eventWithMultipleRelations)).toBe(true);
-46
View File
@@ -1,46 +0,0 @@
import { Filter } from "../../src/filter";
describe("Filter", function() {
const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger";
let filter;
beforeEach(function() {
filter = new Filter(userId);
});
describe("fromJson", function() {
it("create a new Filter from the provided values", function() {
const definition = {
event_fields: ["type", "content"],
};
const f = Filter.fromJson(userId, filterId, definition);
expect(f.getDefinition()).toEqual(definition);
expect(f.userId).toEqual(userId);
expect(f.filterId).toEqual(filterId);
});
});
describe("setTimelineLimit", function() {
it("should set room.timeline.limit of the filter definition", function() {
filter.setTimelineLimit(10);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
limit: 10,
},
},
});
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
const definition = {
event_format: "client",
};
filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition);
});
});
});
+85
View File
@@ -0,0 +1,85 @@
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
import { Filter, IFilterDefinition } from "../../src/filter";
import { mkEvent } from "../test-utils/test-utils";
import { EventType } from "../../src";
describe("Filter", function() {
const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger";
let filter: Filter;
beforeEach(function() {
filter = new Filter(userId);
});
describe("fromJson", function() {
it("create a new Filter from the provided values", function() {
const definition = {
event_fields: ["type", "content"],
};
const f = Filter.fromJson(userId, filterId, definition);
expect(f.getDefinition()).toEqual(definition);
expect(f.userId).toEqual(userId);
expect(f.filterId).toEqual(filterId);
});
});
describe("setTimelineLimit", function() {
it("should set room.timeline.limit of the filter definition", function() {
filter.setTimelineLimit(10);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
limit: 10,
},
},
});
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
const definition = {
event_format: "client" as IFilterDefinition['event_format'],
};
filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition);
});
});
describe("setUnreadThreadNotifications", function() {
it("setUnreadThreadNotifications", function() {
filter.setUnreadThreadNotifications(true);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
[UNREAD_THREAD_NOTIFICATIONS.name]: true,
},
},
});
});
});
describe("filterRoomTimeline", () => {
it("should return input if no roomTimelineFilter and roomFilter", () => {
const events = [mkEvent({ type: EventType.Sticker, content: {}, event: true })];
expect(new Filter(undefined).filterRoomTimeline(events)).toStrictEqual(events);
});
it("should filter using components when present", () => {
const definition: IFilterDefinition = {
room: {
timeline: {
types: [EventType.Sticker],
},
},
};
const filter = Filter.fromJson(userId, filterId, definition);
const events = [
mkEvent({ type: EventType.Sticker, content: {}, event: true }),
mkEvent({ type: EventType.RoomMessage, content: {}, event: true }),
];
expect(filter.filterRoomTimeline(events)).toStrictEqual([events[0]]);
});
});
});
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
{
"base": "http://baseUrl",
"params": {
"access_token": "token",
},
"path": "/_matrix/media/r0/upload",
}
`;
+223
View File
@@ -0,0 +1,223 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
describe("FetchHttpApi", () => {
const baseUrl = "http://baseUrl";
const idBaseUrl = "http://idBaseUrl";
const prefix = ClientPrefix.V3;
it("should support aborting multiple times", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.request(Method.Get, "/foo");
api.request(Method.Get, "/baz");
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
api.request(Method.Get, "/bar");
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
});
it("should fall back to global fetch if fetchFn not provided", () => {
global.fetch = jest.fn();
expect(global.fetch).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
api.fetch("test");
expect(global.fetch).toHaveBeenCalled();
});
it("should update identity server base url", () => {
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(api.opts.idBaseUrl).toBeUndefined();
api.setIdBaseUrl("https://id.foo.bar");
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
});
describe("idServerRequest", () => {
it("should throw if no idBaseUrl", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2))
.toThrow("No identity server base URL set");
});
it("should send params as query string for GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
});
it("should send params as body for non-GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
const params = { foo: "bar", via: ["a", "b"] };
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
});
it("should add Authorization header if token provided", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
});
});
it("should return the Response object if onlyData=false", async () => {
const res = { ok: true };
const fetchFn = jest.fn().mockResolvedValue(res);
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
});
it("should return text if json=false", async () => {
const text = "418 I'm a teapot";
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(api.requestOtherUrl(Method.Get, "http://url", undefined, {
json: false,
})).resolves.toBe(text);
});
it("should send token via query params if useAuthorizationHeader=false", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
});
it("should send token via headers by default", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not send a token if not calling `authedRequest`", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.request(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
});
it("should ensure no token is leaked out via query params if sending via headers", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", { access_token: "123" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not override manually specified access token via query params", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
});
it("should not override manually specified access token via header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Authorization: "Bearer RealToken" },
});
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
});
it("should not override Accept header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Accept: "text/html" },
});
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
});
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
const fetchFn = jest.fn().mockResolvedValue({
ok: false,
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(JSON.stringify({
errcode: "M_CONSENT_NOT_GIVEN",
error: "Ye shall ask for consent",
})),
});
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
await Promise.all([
emitPromise(emitter, HttpApiEvent.NoConsent),
expect(api.authedRequest(Method.Get, "/path")).rejects.toThrow("Ye shall ask for consent"),
]);
});
});
+228
View File
@@ -0,0 +1,228 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import DOMException from "domexception";
import { mocked } from "jest-mock";
import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
jest.useFakeTimers();
describe("MatrixHttpApi", () => {
const baseUrl = "http://baseUrl";
const prefix = ClientPrefix.V3;
let xhr: Partial<Writeable<XMLHttpRequest>>;
let upload: Promise<UploadResponse>;
const DONE = 0;
global.DOMException = DOMException;
beforeEach(() => {
xhr = {
upload: {} as XMLHttpRequestUpload,
open: jest.fn(),
send: jest.fn(),
abort: jest.fn(),
setRequestHeader: jest.fn(),
onreadystatechange: undefined,
getResponseHeader: jest.fn(),
};
// We stub out XHR here as it is not available in JSDOM
// @ts-ignore
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
// @ts-ignore
global.XMLHttpRequest.DONE = DONE;
});
afterEach(() => {
upload?.catch(() => {});
// Abort any remaining requests
xhr.readyState = DONE;
xhr.status = 0;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
});
it("should fall back to `fetch` where xhr is unavailable", () => {
global.XMLHttpRequest = undefined;
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
expect(fetchFn).toHaveBeenCalled();
});
it("should prefer xhr where available", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
expect(fetchFn).not.toHaveBeenCalled();
expect(xhr.open).toHaveBeenCalled();
});
it("should send access token in query params if header disabled", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
accessToken: "token",
useAuthorizationHeader: false,
});
upload = api.uploadContent({} as File);
expect(xhr.open)
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token");
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
});
it("should send access token in header by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
accessToken: "token",
});
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
});
it("should include filename by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File, { name: "name" });
expect(xhr.open)
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name");
});
it("should allow not sending the filename", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
});
it("should abort xhr when the upload is aborted", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
api.cancelUpload(upload);
expect(xhr.abort).toHaveBeenCalled();
return expect(upload).rejects.toThrow("Aborted");
});
it("should timeout if no progress in 30s", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
jest.advanceTimersByTime(25000);
// @ts-ignore
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
jest.advanceTimersByTime(25000);
expect(xhr.abort).not.toHaveBeenCalled();
jest.advanceTimersByTime(5000);
expect(xhr.abort).toHaveBeenCalled();
});
it("should call progressHandler", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
const progressHandler = jest.fn();
upload = api.uploadContent({} as File, { progressHandler });
const progressEvent = new Event("progress") as ProgressEvent;
Object.assign(progressEvent, { loaded: 1, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
Object.assign(progressEvent, { loaded: 95, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
});
it("should error when no response body", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = "";
xhr.status = 200;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("No response body.");
});
it("should error on a 400-code", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
xhr.status = 404;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("Not found");
});
it("should return response on successful upload", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
xhr.status = 200;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
});
it("should abort xhr when calling `cancelUpload`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
expect(api.cancelUpload(upload)).toBeTruthy();
expect(xhr.abort).toHaveBeenCalled();
});
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.status = 500;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
await upload.catch(() => {});
expect(api.cancelUpload(upload)).toBeFalsy();
expect(xhr.abort).not.toHaveBeenCalled();
});
it("should return active uploads in `getCurrentUploads`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeTruthy();
api.cancelUpload(upload);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeFalsy();
});
it("should return expected object from `getContentUri`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, accessToken: "token" });
expect(api.getContentUri()).toMatchSnapshot();
});
});
+219
View File
@@ -0,0 +1,219 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import {
anySignal,
ConnectionError,
HTTPError,
MatrixError,
parseErrorResponse,
retryNetworkOperation,
timeoutSignal,
} from "../../../src";
import { sleep } from "../../../src/utils";
jest.mock("../../../src/utils");
describe("timeoutSignal", () => {
jest.useFakeTimers();
it("should fire abort signal after specified timeout", () => {
const signal = timeoutSignal(3000);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(3000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
});
describe("anySignal", () => {
jest.useFakeTimers();
it("should fire when any signal fires", () => {
const { signal } = anySignal([
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(2000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
it("should cleanup when instructed", () => {
const { signal, cleanup } = anySignal([
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
cleanup();
jest.advanceTimersByTime(2000);
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
});
it("should abort immediately if passed an aborted signal", () => {
const controller = new AbortController();
controller.abort();
const { signal } = anySignal([controller.signal]);
expect(signal.aborted).toBeTruthy();
});
});
describe("parseErrorResponse", () => {
it("should resolve Matrix Errors from XHR", () => {
expect(parseErrorResponse({
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
status: 500,
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500));
});
it("should resolve Matrix Errors from fetch", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500));
});
it("should resolve Matrix Errors from XHR with urls", () => {
expect(parseErrorResponse({
responseURL: "https://example.com",
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
status: 500,
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500, "https://example.com"));
});
it("should resolve Matrix Errors from fetch with urls", () => {
expect(parseErrorResponse({
url: "https://example.com",
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500, "https://example.com"));
});
it("should set a sensible default error message on MatrixError", () => {
let err = new MatrixError();
expect(err.message).toEqual("MatrixError: Unknown message");
err = new MatrixError({
error: "Oh no",
});
expect(err.message).toEqual("MatrixError: Oh no");
});
it("should handle no type gracefully", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new HTTPError("Server returned 500 error", 500));
});
it("should handle invalid type gracefully", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? " " : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}'))
.toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
});
it("should handle plaintext errors", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "text/plain" : null;
},
},
status: 418,
} as Response, "I'm a teapot")).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418));
});
});
describe("retryNetworkOperation", () => {
it("should retry given number of times with exponential sleeps", async () => {
const err = new ConnectionError("test");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(4);
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
});
it("should bail out on errors other than ConnectionError", async () => {
const err = new TypeError("invalid JSON");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(1);
});
it("should return newest ConnectionError when giving up", async () => {
const err1 = new ConnectionError("test1");
const err2 = new ConnectionError("test2");
const err3 = new ConnectionError("test3");
const errors = [err1, err2, err3];
const fn = jest.fn().mockImplementation(() => {
throw errors.shift();
});
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
});
});
-175
View File
@@ -1,175 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../../src/logger";
import { InteractiveAuth } from "../../src/interactive-auth";
import { MatrixError } from "../../src/http-api";
// Trivial client object to test interactive auth
// (we do not need TestClient here)
class FakeClient {
generateClientSecret() {
return "testcl1Ent5EcreT";
}
}
describe("InteractiveAuth", function() {
it("should start an auth stage and complete it", function() {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
authData: {
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation(function(stage) {
logger.log('aaaa');
expect(stage).toEqual("logintype");
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(function(authData) {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return Promise.resolve(requestRes);
});
return ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
});
it("should make a request if no authdata is provided", function() {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
stateUpdated: stateUpdated,
doRequest: doRequest,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams("logintype")).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation(function(authData) {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation(function(stage) {
expect(stage).toEqual("logintype");
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(function(authData) {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return Promise.resolve(requestRes);
});
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
return ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
});
it("should start an auth stage and reject if no auth flow", function() {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
});
doRequest.mockImplementation(function(authData) {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
"logintype": { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
return ia.attemptAuth().catch(function(error) {
expect(error.message).toBe('No appropriate authentication flow found');
});
});
});
+546
View File
@@ -0,0 +1,546 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "../../src/client";
import { logger } from "../../src/logger";
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
import { HTTPError, MatrixError } from "../../src/http-api";
import { sleep } from "../../src/utils";
import { randomString } from "../../src/randomstring";
// Trivial client object to test interactive auth
// (we do not need TestClient here)
class FakeClient {
generateClientSecret() {
return "testcl1Ent5EcreT";
}
}
const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixClient;
describe("InteractiveAuth", () => {
it("should start an auth stage and complete it", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
params: {
[AuthType.Password]: { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('aaaa');
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
type: AuthType.Password,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should handle auth errcode presence ", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
errcode: "MockError0",
params: {
[AuthType.Password]: { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('aaaa');
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
type: AuthType.Password,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should handle set emailSid for email flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
doRequest,
stateUpdated,
requestEmailToken,
matrixClient: getFakeClient(),
emailSid: 'myEmailSid',
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Email, AuthType.Password] },
],
params: {
[AuthType.Email]: { param: "aa" },
[AuthType.Password]: { param: "bb" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Email)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('husky');
expect(stage).toEqual(AuthType.Email);
ia.submitAuthDict({
type: AuthType.Email,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('barfoo');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Email,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
expect(requestEmailToken).toBeCalledTimes(0);
expect(ia.getEmailSid()).toBe("myEmailSid");
});
it("should make a request if no authdata is provided", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
stateUpdated,
doRequest,
requestEmailToken,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
params: {
[AuthType.Password]: { param: "aa" },
},
}, 401);
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
ia.submitAuthDict({
type: AuthType.Password,
});
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should make a request if authdata is null", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
stateUpdated,
doRequest,
requestEmailToken,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
params: {
[AuthType.Password]: { param: "aa" },
},
}, 401);
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
ia.submitAuthDict({
type: AuthType.Password,
});
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should start an auth stage and reject if no auth flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
[AuthType.Password]: { param: "aa" },
},
}, 401);
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error('No appropriate authentication flow found'),
);
});
it("should start an auth stage and reject if no auth flow but has session", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
},
sessionId: "sessionId",
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
[AuthType.Password]: { param: "aa" },
},
error: "Mock Error 1",
errcode: "MOCKERR1",
}, 401);
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error('No appropriate authentication flow found'),
);
});
it("should handle unexpected error types without data propery set", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: "sessionId",
},
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new HTTPError('myerror', 401);
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error("myerror"),
);
});
it("should allow dummy auth", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: 'sessionId',
flows: [
{ stages: [AuthType.Dummy] },
],
params: {},
},
});
const requestRes = { "a": "b" };
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Dummy,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(0);
});
describe("requestEmailToken", () => {
it("increases auth attempts", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
});
it("increases auth attempts", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
});
it("passes errors through", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
requestEmailToken.mockImplementation(async () => {
throw new Error("unspecific network error");
});
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
});
it("only starts one request at a time", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await Promise.all([ia.requestEmailToken(), ia.requestEmailToken(), ia.requestEmailToken()]);
expect(requestEmailToken).toHaveBeenCalledTimes(1);
});
it("stores result in email sid", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const sid = randomString(24);
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await ia.requestEmailToken();
expect(ia.getEmailSid()).toEqual(sid);
});
});
});
+43
View File
@@ -0,0 +1,43 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { LocalNotificationSettings } from "../../src/@types/local_notifications";
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix";
import { TestClient } from '../TestClient';
let client: MatrixClient;
describe("Local notification settings", () => {
beforeEach(() => {
client = (new TestClient(
"@alice:matrix.org", "123", undefined, undefined, undefined,
)).client;
client.setAccountData = jest.fn();
});
describe("Lets you set local notification settings", () => {
it("stores settings in account data", () => {
const deviceId = "device";
const settings: LocalNotificationSettings = { is_silenced: true };
client.setLocalNotificationSettings(deviceId, settings);
expect(client.setAccountData).toHaveBeenCalledWith(
`${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`,
settings,
);
});
});
});
-24
View File
@@ -1,24 +0,0 @@
import { TestClient } from '../TestClient';
describe('Login request', function() {
let client;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
it('should store "access_token" and "user_id" if in response', async function() {
const response = { user_id: 1, access_token: Date.now().toString(16) };
client.httpBackend.when('POST', '/login').respond(200, response);
client.httpBackend.flush('/login', 1, 100);
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
expect(client.client.getAccessToken()).toBe(response.access_token);
expect(client.client.getUserId()).toBe(response.user_id);
});
});
+59
View File
@@ -0,0 +1,59 @@
import { SSOAction } from '../../src/@types/auth';
import { TestClient } from '../TestClient';
describe('Login request', function() {
let client: TestClient;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
it('should store "access_token" and "user_id" if in response', async function() {
const response = { user_id: 1, access_token: Date.now().toString(16) };
client.httpBackend.when('POST', '/login').respond(200, response);
client.httpBackend.flush('/login', 1, 100);
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
expect(client.client.getAccessToken()).toBe(response.access_token);
expect(client.client.getUserId()).toBe(response.user_id);
});
});
describe('SSO login URL', function() {
let client: TestClient;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
describe('SSOAction', function() {
const redirectUri = "https://test.com/foo";
it('No action', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
const url = new URL(urlString);
expect(url.searchParams.has('org.matrix.msc3824.action')).toBe(false);
});
it('register', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('register');
});
it('login', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('login');
});
});
});
+644 -49
View File
@@ -14,8 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { logger } from "../../src/logger";
import { MatrixClient } from "../../src/client";
import { MatrixClient, ClientEvent } from "../../src/client";
import { Filter } from "../../src/filter";
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
import {
@@ -27,16 +29,29 @@ import {
UNSTABLE_MSC3089_TREE_SUBTYPE,
} from "../../src/@types/event";
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
import { Crypto } from "../../src/crypto";
import { EventStatus, MatrixEvent } from "../../src/models/event";
import { Preset } from "../../src/@types/partials";
import { ReceiptType } from "../../src/@types/read_receipts";
import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { Room } from "../../src";
import { ContentHelpers, EventTimeline, Room } from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
import {
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyScope,
} from "../../src/models/invites-ignorer";
jest.useFakeTimers();
jest.mock("../../src/webrtc/call", () => ({
...jest.requireActual("../../src/webrtc/call"),
supportsMatrixCall: jest.fn(() => false),
}));
describe("MatrixClient", function() {
const userId = "@alice:bar";
const identityServerUrl = "https://identity.server";
@@ -86,11 +101,16 @@ describe("MatrixClient", function() {
// }
// items are popped off when processed and block if no items left.
];
let acceptKeepalives;
let acceptKeepalives: boolean;
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
function httpReq(method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return Promise.resolve();
return Promise.resolve({
unstable_features: {
"org.matrix.msc3440.stable": true,
},
versions: ["r0.6.0", "r0.6.1"],
});
}
const next = httpLookups.shift();
const logLine = (
@@ -120,7 +140,7 @@ describe("MatrixClient", function() {
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
expect(data).toEqual(next.expectBody);
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
@@ -144,10 +164,32 @@ describe("MatrixClient", function() {
}
return Promise.resolve(next.data);
}
// Jest doesn't let us have custom expectation errors, so if you're seeing this then
// you forgot to handle at least 1 pending request. Check your tests to ensure your
// number of expectations lines up with your number of requests made, and that those
// requests match your expectations.
expect(true).toBe(false);
return new Promise(() => {});
}
function makeClient() {
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
fetchFn: function() {} as any, // NOP
store: store,
scheduler: scheduler,
userId: userId,
});
// FIXME: We shouldn't be yanking http like this.
client.http = [
"authedRequest", "getContentUri", "request", "uploadContent",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
client.http.authedRequest.mockImplementation(httpReq);
client.http.request.mockImplementation(httpReq);
}
beforeEach(function() {
scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
@@ -165,21 +207,7 @@ describe("MatrixClient", function() {
store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true));
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
request: function() {} as any, // NOP
store: store,
scheduler: scheduler,
userId: userId,
});
// FIXME: We shouldn't be yanking http like this.
client.http = [
"authedRequest", "getContentUri", "request", "uploadContent",
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
client.http.authedRequest.mockImplementation(httpReq);
client.http.request.mockImplementation(httpReq);
makeClient();
// set reasonable working defaults
acceptKeepalives = true;
@@ -199,6 +227,7 @@ describe("MatrixClient", function() {
client.http.authedRequest.mockImplementation(function() {
return new Promise(() => {});
});
client.stopClient();
});
it("should create (unstable) file trees", async () => {
@@ -402,7 +431,7 @@ describe("MatrixClient", function() {
}
});
});
await client.startClient();
await client.startClient({ filter });
await syncPromise;
});
@@ -719,18 +748,16 @@ describe("MatrixClient", function() {
});
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filterw
it("should only do /sync calls (without filter/pushrules)", async function() {
httpLookups = []; // no /pushrules or /filter
httpLookups.push({
method: "GET",
path: "/sync",
data: SYNC_DATA,
thenCall: function() {
done();
},
});
client.setGuest(true);
client.startClient();
await client.startClient();
expect(httpLookups.length).toBe(0);
});
xit("should be able to peek into a room using peekInRoom", function(done) {
@@ -767,7 +794,7 @@ describe("MatrixClient", function() {
expectBody: content,
}];
await client.sendEvent(roomId, EventType.RoomMessage, content, txnId);
await client.sendEvent(roomId, EventType.RoomMessage, { ...content }, txnId);
});
it("overload with null threadId works", async () => {
@@ -780,20 +807,99 @@ describe("MatrixClient", function() {
expectBody: content,
}];
await client.sendEvent(roomId, null, EventType.RoomMessage, content, txnId);
await client.sendEvent(roomId, null, EventType.RoomMessage, { ...content }, txnId);
});
it("overload with threadId works", async () => {
const eventId = "$eventId:example.org";
const txnId = client.makeTxnId();
const threadId = "$threadId:server";
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: content,
expectBody: {
...content,
"m.relates_to": {
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
},
},
}];
await client.sendEvent(roomId, "$threadId:server", EventType.RoomMessage, content, txnId);
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
});
it("should add thread relation if threadId is passed and the relation is missing", async () => {
const eventId = "$eventId:example.org";
const threadId = "$threadId:server";
const txnId = client.makeTxnId();
const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: threadId,
},
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
},
},
}];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
});
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
const eventId = "$eventId:example.org";
const threadId = "$threadId:server";
const txnId = client.makeTxnId();
const content = {
body,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
},
};
const room = new Room(roomId, client, userId);
store.getRoom.mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
httpLookups = [{
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
data: { event_id: eventId },
expectBody: {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
"event_id": threadId,
"is_falling_back": false,
"rel_type": "m.thread",
},
},
}];
await client.sendEvent(roomId, threadId, EventType.RoomMessage, { ...content }, txnId);
});
});
@@ -920,6 +1026,7 @@ describe("MatrixClient", function() {
};
client.crypto = { // mock crypto
encryptEvent: (event, room) => new Promise(() => {}),
stop: jest.fn(),
};
});
@@ -992,6 +1099,46 @@ describe("MatrixClient", function() {
});
});
describe("read-markers and read-receipts", () => {
it("setRoomReadMarkers", () => {
client.setRoomReadMarkersHttpRequest = jest.fn();
const room = {
hasPendingEvent: jest.fn().mockReturnValue(false),
addLocalEchoReceipt: jest.fn(),
};
const rrEvent = new MatrixEvent({ event_id: "read_event_id" });
const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" });
client.getRoom = () => room;
client.setRoomReadMarkers(
"room_id",
"read_marker_event_id",
rrEvent,
rpEvent,
);
expect(client.setRoomReadMarkersHttpRequest).toHaveBeenCalledWith(
"room_id",
"read_marker_event_id",
"read_event_id",
"read_private_event_id",
);
expect(room.addLocalEchoReceipt).toHaveBeenCalledTimes(2);
expect(room.addLocalEchoReceipt).toHaveBeenNthCalledWith(
1,
client.credentials.userId,
rrEvent,
ReceiptType.Read,
);
expect(room.addLocalEchoReceipt).toHaveBeenNthCalledWith(
2,
client.credentials.userId,
rpEvent,
ReceiptType.ReadPrivate,
);
});
});
describe("beacons", () => {
const roomId = '!room:server.org';
const content = makeBeaconInfoContent(100, true);
@@ -1005,8 +1152,7 @@ describe("MatrixClient", function() {
// event type combined
const expectedEventType = M_BEACON_INFO.name;
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy();
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(method).toBe('PUT');
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
@@ -1020,7 +1166,7 @@ describe("MatrixClient", function() {
await client.unstable_setLiveBeacon(roomId, content);
// event type combined
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
const [, path, , requestContent] = client.http.authedRequest.mock.calls[0];
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
@@ -1058,18 +1204,47 @@ describe("MatrixClient", function() {
});
});
describe("setRoomTopic", () => {
const roomId = "!foofoofoofoofoofoo:matrix.org";
const createSendStateEventMock = (topic: string, htmlTopic?: string) => {
return jest.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(roomId);
expect(eventType).toEqual(EventType.RoomTopic);
expect(content).toMatchObject(ContentHelpers.makeTopicContent(topic, htmlTopic));
expect(stateKey).toBeUndefined();
return Promise.resolve();
});
};
it("is called with plain text topic and sends state event", async () => {
const sendStateEvent = createSendStateEventMock("pizza");
client.sendStateEvent = sendStateEvent;
await client.setRoomTopic(roomId, "pizza");
expect(sendStateEvent).toHaveBeenCalledTimes(1);
});
it("is called with plain text topic and callback and sends state event", async () => {
const sendStateEvent = createSendStateEventMock("pizza");
client.sendStateEvent = sendStateEvent;
await client.setRoomTopic(roomId, "pizza");
expect(sendStateEvent).toHaveBeenCalledTimes(1);
});
it("is called with plain text and HTML topic and sends state event", async () => {
const sendStateEvent = createSendStateEventMock("pizza", "<b>pizza</b>");
client.sendStateEvent = sendStateEvent;
await client.setRoomTopic(roomId, "pizza", "<b>pizza</b>");
expect(sendStateEvent).toHaveBeenCalledTimes(1);
});
});
describe("setPassword", () => {
const auth = { session: 'abcdef', type: 'foo' };
const newPassword = 'newpassword';
const callback = () => {};
const passwordTest = (expectedRequestContent: any, expectedCallback?: Function) => {
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
if (expectedCallback) {
expect(callback).toBe(expectedCallback);
} else {
expect(callback).toBeFalsy();
}
const passwordTest = (expectedRequestContent: any) => {
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(method).toBe('POST');
expect(path).toEqual('/account/password');
expect(queryParams).toBeFalsy();
@@ -1086,8 +1261,8 @@ describe("MatrixClient", function() {
});
it("no logout_devices specified + callback", async () => {
await client.setPassword(auth, newPassword, callback);
passwordTest({ auth, new_password: newPassword }, callback);
await client.setPassword(auth, newPassword);
passwordTest({ auth, new_password: newPassword });
});
it("overload logoutDevices=true", async () => {
@@ -1096,8 +1271,8 @@ describe("MatrixClient", function() {
});
it("overload logoutDevices=true + callback", async () => {
await client.setPassword(auth, newPassword, true, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: true }, callback);
await client.setPassword(auth, newPassword, true);
passwordTest({ auth, new_password: newPassword, logout_devices: true });
});
it("overload logoutDevices=false", async () => {
@@ -1106,8 +1281,428 @@ describe("MatrixClient", function() {
});
it("overload logoutDevices=false + callback", async () => {
await client.setPassword(auth, newPassword, false, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback);
await client.setPassword(auth, newPassword, false);
passwordTest({ auth, new_password: newPassword, logout_devices: false });
});
});
describe("getLocalAliases", () => {
it("should call the right endpoint", async () => {
const response = {
aliases: ["#woop:example.org", "#another:example.org"],
};
client.http.authedRequest.mockClear().mockResolvedValue(response);
const roomId = "!whatever:example.org";
const result = await client.getLocalAliases(roomId);
// Current version of the endpoint we support is v3
const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
expect(data).toBeFalsy();
expect(method).toBe('GET');
expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`);
expect(opts).toMatchObject({ prefix: "/_matrix/client/v3" });
expect(queryParams).toBeFalsy();
expect(result!.aliases).toEqual(response.aliases);
});
});
describe("pollingTurnServers", () => {
afterEach(() => {
mocked(supportsMatrixCall).mockReset();
});
it("is false if the client isn't started", () => {
expect(client.clientRunning).toBe(false);
expect(client.pollingTurnServers).toBe(false);
});
it("is false if VoIP is not supported", async () => {
mocked(supportsMatrixCall).mockReturnValue(false);
makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock
await client.startClient();
expect(client.pollingTurnServers).toBe(false);
});
it("is true if VoIP is supported", async () => {
mocked(supportsMatrixCall).mockReturnValue(true);
makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock
await client.startClient();
expect(client.pollingTurnServers).toBe(true);
});
});
describe("checkTurnServers", () => {
beforeAll(() => {
mocked(supportsMatrixCall).mockReturnValue(true);
});
beforeEach(() => {
makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock
});
afterAll(() => {
mocked(supportsMatrixCall).mockReset();
});
it("emits an event when new TURN creds are found", async () => {
const turnServer = {
uris: [
"turn:turn.example.com:3478?transport=udp",
"turn:10.20.30.40:3478?transport=tcp",
"turns:10.20.30.40:443?transport=tcp",
],
username: "1443779631:@user:example.com",
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
};
jest.spyOn(client, "turnServer").mockResolvedValue(turnServer);
const events: any[][] = [];
const onTurnServers = (...args) => events.push(args);
client.on(ClientEvent.TurnServers, onTurnServers);
expect(await client.checkTurnServers()).toBe(true);
client.off(ClientEvent.TurnServers, onTurnServers);
expect(events).toEqual([[[{
urls: turnServer.uris,
username: turnServer.username,
credential: turnServer.password,
}]]]);
});
it("emits an event when an error occurs", async () => {
const error = new Error(":(");
jest.spyOn(client, "turnServer").mockRejectedValue(error);
const events: any[][] = [];
const onTurnServersError = (...args) => events.push(args);
client.on(ClientEvent.TurnServersError, onTurnServersError);
expect(await client.checkTurnServers()).toBe(false);
client.off(ClientEvent.TurnServersError, onTurnServersError);
expect(events).toEqual([[error, false]]); // non-fatal
});
it("considers 403 errors fatal", async () => {
const error = { httpStatus: 403 };
jest.spyOn(client, "turnServer").mockRejectedValue(error);
const events: any[][] = [];
const onTurnServersError = (...args) => events.push(args);
client.on(ClientEvent.TurnServersError, onTurnServersError);
expect(await client.checkTurnServers()).toBe(false);
client.off(ClientEvent.TurnServersError, onTurnServersError);
expect(events).toEqual([[error, true]]); // fatal
});
});
describe("encryptAndSendToDevices", () => {
it("throws an error if crypto is unavailable", () => {
client.crypto = undefined;
expect(() => client.encryptAndSendToDevices([], {})).toThrow();
});
it("is an alias for the crypto method", async () => {
client.crypto = testUtils.mock(Crypto, "Crypto");
const deviceInfos = [];
const payload = {};
await client.encryptAndSendToDevices(deviceInfos, payload);
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
});
});
describe("support for ignoring invites", () => {
beforeEach(() => {
// Mockup `getAccountData`/`setAccountData`.
const dataStore = new Map();
client.setAccountData = function(eventType, content) {
dataStore.set(eventType, content);
return Promise.resolve();
};
client.getAccountData = function(eventType) {
const data = dataStore.get(eventType);
return new MatrixEvent({
content: data,
});
};
// Mockup `createRoom`/`getRoom`/`joinRoom`, including state.
const rooms = new Map();
client.createRoom = function(options = {}) {
const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`;
const state = new Map();
const room = {
roomId,
_options: options,
_state: state,
getUnfilteredTimelineSet: function() {
return {
getLiveTimeline: function() {
return {
getState: function(direction) {
expect(direction).toBe(EventTimeline.FORWARDS);
return {
getStateEvents: function(type) {
const store = state.get(type) || {};
return Object.keys(store).map(key => store[key]);
},
};
},
};
},
};
},
};
rooms.set(roomId, room);
return Promise.resolve({ room_id: roomId });
};
client.getRoom = function(roomId) {
return rooms.get(roomId);
};
client.joinRoom = function(roomId) {
return this.getRoom(roomId) || this.createRoom({ _roomId: roomId });
};
// Mockup state events
client.sendStateEvent = function(roomId, type, content) {
const room = this.getRoom(roomId);
const state: Map<string, any> = room._state;
let store = state.get(type);
if (!store) {
store = {};
state.set(type, store);
}
const eventId = `$event-${Math.random()}:example.org`;
store[eventId] = {
getId: function() {
return eventId;
},
getRoomId: function() {
return roomId;
},
getContent: function() {
return content;
},
};
return { event_id: eventId };
};
client.redactEvent = function(roomId, eventId) {
const room = this.getRoom(roomId);
const state: Map<string, any> = room._state;
for (const store of state.values()) {
delete store[eventId];
}
};
});
it("should initialize and return the same `target` consistently", async () => {
const target1 = await client.ignoredInvites.getOrCreateTargetRoom();
const target2 = await client.ignoredInvites.getOrCreateTargetRoom();
expect(target1).toBeTruthy();
expect(target1).toBe(target2);
});
it("should initialize and return the same `sources` consistently", async () => {
const sources1 = await client.ignoredInvites.getOrCreateSourceRooms();
const sources2 = await client.ignoredInvites.getOrCreateSourceRooms();
expect(sources1).toBeTruthy();
expect(sources1).toHaveLength(1);
expect(sources1).toEqual(sources2);
});
it("should initially not reject any invite", async () => {
const rule = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(rule).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: user)", async () => {
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleWrongServerRoom).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: server)", async () => {
const REASON = `Just a test ${Math.random()}`;
await client.ignoredInvites.addRule(PolicyScope.Server, "example.org", REASON);
// We should reject these invites.
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
const ruleRoomMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleRoomMatch).toBeTruthy();
expect(ruleRoomMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: room)", async () => {
const REASON = `Just a test ${Math.random()}`;
const BAD_ROOM_ID = "!bad:example.org";
const GOOD_ROOM_ID = "!good:example.org";
await client.ignoredInvites.addRule(PolicyScope.Room, BAD_ROOM_ID, REASON);
// We should reject this invite.
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: BAD_ROOM_ID,
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
// We should let these invites go through.
const ruleWrongRoom = await client.ignoredInvites.getRuleForInvite({
sender: BAD_ROOM_ID,
roomId: GOOD_ROOM_ID,
});
expect(ruleWrongRoom).toBeFalsy();
});
it("should reject invites once we have added a matching rule in a non-target source room", async () => {
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
// Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID);
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
// Add a rule in the new source room.
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, {
entity: "*:example.org",
reason: "just a test",
recommendation: "m.ban",
});
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleWrongServerRoom).toBeFalsy();
});
it("should not reject invites anymore once we have removed a rule", async () => {
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// After removing the invite, we shouldn't reject it anymore.
await client.ignoredInvites.removeRule(ruleMatch);
const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch2).toBeFalsy();
});
it("should add new rules in the target room, rather than any other source room", async () => {
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
// Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID);
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID);
// Fetch the list of sources and check that we do not have the new room yet.
const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
expect(policies).toBeTruthy();
const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites).toBeTruthy();
expect(ignoreInvites.sources).toBeTruthy();
expect(ignoreInvites.sources).not.toContain(NEW_SOURCE_ROOM_ID);
// Add a source.
const added = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
expect(added).toBe(true);
const added2 = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
expect(added2).toBe(false);
// Fetch the list of sources and check that we have added the new room.
const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
expect(policies2).toBeTruthy();
const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites2).toBeTruthy();
expect(ignoreInvites2.sources).toBeTruthy();
expect(ignoreInvites2.sources).toContain(NEW_SOURCE_ROOM_ID);
// Add a rule.
const eventId = await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// Check where it shows up.
const targetRoomId = ignoreInvites2.target;
const targetRoom = client.getRoom(targetRoomId);
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
});
});
});
+3 -5
View File
@@ -565,7 +565,7 @@ describe("MSC3089TreeSpace", () => {
rooms = {};
rooms[tree.roomId] = parentRoom;
(<any>tree).room = parentRoom; // override readonly
client.getRoom = (r) => rooms[r];
client.getRoom = (r) => rooms[r ?? ""];
clientSendStateFn = jest.fn()
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
@@ -890,9 +890,8 @@ describe("MSC3089TreeSpace", () => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
});
return Promise.resolve(mxc);
return Promise.resolve({ content_uri: mxc });
});
client.uploadContent = uploadFn;
@@ -950,9 +949,8 @@ describe("MSC3089TreeSpace", () => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
});
return Promise.resolve(mxc);
return Promise.resolve({ content_uri: mxc });
});
client.uploadContent = uploadFn;
+217 -19
View File
@@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { MatrixEvent } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import {
isTimestampInDuration,
Beacon,
@@ -65,33 +69,36 @@ describe('Beacon', () => {
// beacon_info events
// created 'an hour ago'
// without timeout of 3 hours
let liveBeaconEvent;
let notLiveBeaconEvent;
let user2BeaconEvent;
let liveBeaconEvent: MatrixEvent;
let notLiveBeaconEvent: MatrixEvent;
let user2BeaconEvent: MatrixEvent;
const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
jest.spyOn(global.Date, 'now').mockReturnValue(now + ms);
jest.spyOn(global.Date, 'now').mockReturnValue(Date.now() + ms);
// then advance time for the interval by the same amount
jest.advanceTimersByTime(ms);
};
beforeEach(() => {
// go back in time to create the beacon
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS);
liveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
timestamp: now - HOUR_MS,
},
'$live123',
);
notLiveBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{ timeout: HOUR_MS * 3, isLive: false },
{
timeout: HOUR_MS * 3,
isLive: false,
timestamp: now - HOUR_MS,
},
'$dead123',
);
user2BeaconEvent = makeBeaconInfoEvent(
@@ -100,11 +107,12 @@ describe('Beacon', () => {
{
timeout: HOUR_MS * 3,
isLive: true,
timestamp: now - HOUR_MS,
},
'$user2live123',
);
// back to now
// back to 'now'
jest.spyOn(global.Date, 'now').mockReturnValue(now);
});
@@ -124,6 +132,24 @@ describe('Beacon', () => {
expect(beacon.beaconInfo).toBeTruthy();
});
it('creates beacon without error from a malformed event', () => {
const event = new MatrixEvent({
type: M_BEACON_INFO.name,
room_id: roomId,
state_key: userId,
content: {},
});
const beacon = new Beacon(event);
expect(beacon.beaconInfoId).toEqual(event.getId());
expect(beacon.roomId).toEqual(roomId);
expect(beacon.isLive).toEqual(false);
expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
expect(beacon.beaconInfo).toBeTruthy();
});
describe('isLive()', () => {
it('returns false when beacon is explicitly set to not live', () => {
const beacon = new Beacon(notLiveBeaconEvent);
@@ -131,17 +157,81 @@ describe('Beacon', () => {
});
it('returns false when beacon is expired', () => {
// time travel to beacon creation + 3 hours
jest.spyOn(global.Date, 'now').mockReturnValue(now - 3 * HOUR_MS);
const beacon = new Beacon(liveBeaconEvent);
const expiredBeaconEvent = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now - HOUR_MS * 2,
},
'$user2live123',
);
const beacon = new Beacon(expiredBeaconEvent);
expect(beacon.isLive).toEqual(false);
});
it('returns false when beacon timestamp is in future', () => {
// time travel to before beacon events timestamp
// event was created now - 1 hour
jest.spyOn(global.Date, 'now').mockReturnValue(now - HOUR_MS - HOUR_MS);
const beacon = new Beacon(liveBeaconEvent);
it('returns false when beacon timestamp is in future by an hour', () => {
const beaconStartsInHour = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now + HOUR_MS,
},
'$user2live123',
);
const beacon = new Beacon(beaconStartsInHour);
expect(beacon.isLive).toEqual(false);
});
it('returns true when beacon timestamp is one minute in the future', () => {
const beaconStartsInOneMin = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now + 60000,
},
'$user2live123',
);
const beacon = new Beacon(beaconStartsInOneMin);
expect(beacon.isLive).toEqual(true);
});
it('returns true when beacon timestamp is one minute before expiry', () => {
// this test case is to check the start time leniency doesn't affect
// strict expiry time checks
const expiresInOneMin = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now - HOUR_MS + 60000,
},
'$user2live123',
);
const beacon = new Beacon(expiresInOneMin);
expect(beacon.isLive).toEqual(true);
});
it('returns false when beacon timestamp is one minute after expiry', () => {
// this test case is to check the start time leniency doesn't affect
// strict expiry time checks
const expiredOneMinAgo = makeBeaconInfoEvent(
userId2,
roomId,
{
timeout: HOUR_MS,
isLive: true,
timestamp: now - HOUR_MS - 60000,
},
'$user2live123',
);
const beacon = new Beacon(expiredOneMinAgo);
expect(beacon.isLive).toEqual(false);
});
@@ -224,13 +314,47 @@ describe('Beacon', () => {
beacon.monitorLiveness();
// @ts-ignore
expect(beacon.livenessWatchInterval).toBeFalsy();
expect(beacon.livenessWatchTimeout).toBeFalsy();
advanceDateAndTime(HOUR_MS * 2 + 1);
// no emit
expect(emitSpy).not.toHaveBeenCalled();
});
it('checks liveness of beacon at expected start time', () => {
const futureBeaconEvent = makeBeaconInfoEvent(
userId,
roomId,
{
timeout: HOUR_MS * 3,
isLive: true,
// start timestamp hour in future
timestamp: now + HOUR_MS,
},
'$live123',
);
const beacon = new Beacon(futureBeaconEvent);
expect(beacon.isLive).toBeFalsy();
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.monitorLiveness();
// advance to the start timestamp of the beacon
advanceDateAndTime(HOUR_MS + 1);
// beacon is in live period now
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, true, beacon);
// check the expiry monitor is still setup ok
// advance to the expiry
advanceDateAndTime(HOUR_MS * 3 + 100);
expect(emitSpy).toHaveBeenCalledTimes(2);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LivenessChange, false, beacon);
});
it('checks liveness of beacon at expected expiry time', () => {
// live beacon was created an hour ago
// and has a 3hr duration
@@ -253,12 +377,12 @@ describe('Beacon', () => {
beacon.monitorLiveness();
// @ts-ignore
const oldMonitor = beacon.livenessWatchInterval;
const oldMonitor = beacon.livenessWatchTimeout;
beacon.monitorLiveness();
// @ts-ignore
expect(beacon.livenessWatchInterval).not.toEqual(oldMonitor);
expect(beacon.livenessWatchTimeout).not.toEqual(oldMonitor);
});
it('destroy kills liveness monitor and emits', () => {
@@ -309,6 +433,78 @@ describe('Beacon', () => {
expect(emitSpy).not.toHaveBeenCalled();
});
it("should ignore invalid beacon events", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit');
const ev = new MatrixEvent({
type: M_BEACON_INFO.name,
sender: userId,
room_id: roomId,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beacon.beaconInfoId,
},
},
});
beacon.addLocations([ev]);
expect(beacon.latestLocationEvent).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
describe('when beacon is live with a start timestamp is in the future', () => {
it('ignores locations before the beacon start timestamp', () => {
const startTimestamp = now + 60000;
const beacon = new Beacon(makeBeaconInfoEvent(
userId,
roomId,
{ isLive: true, timeout: 60000, timestamp: startTimestamp },
));
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.addLocations([
// beacon has now + 60000 live period
makeBeaconEvent(
userId,
{
beaconInfoId: beacon.beaconInfoId,
// now < location timestamp < beacon timestamp
timestamp: now + 10,
},
),
]);
expect(beacon.latestLocationState).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
it('sets latest location when location timestamp is after startTimestamp', () => {
const startTimestamp = now + 60000;
const beacon = new Beacon(makeBeaconInfoEvent(
userId,
roomId,
{ isLive: true, timeout: 600000, timestamp: startTimestamp },
));
const emitSpy = jest.spyOn(beacon, 'emit');
beacon.addLocations([
// beacon has now + 600000 live period
makeBeaconEvent(
userId,
{
beaconInfoId: beacon.beaconInfoId,
// now < beacon timestamp < location timestamp
timestamp: startTimestamp + 10,
},
),
]);
expect(beacon.latestLocationState).toBeTruthy();
expect(emitSpy).toHaveBeenCalled();
});
});
it('sets latest location state to most recent location', () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit');
@@ -338,6 +534,7 @@ describe('Beacon', () => {
// the newest valid location
expect(beacon.latestLocationState).toEqual(expectedLatestLocation);
expect(beacon.latestLocationEvent).toEqual(locations[1]);
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.LocationUpdate, expectedLatestLocation);
});
@@ -356,6 +553,7 @@ describe('Beacon', () => {
expect(beacon.latestLocationState).toEqual(expect.objectContaining({
uri: 'geo:bar',
}));
expect(beacon.latestLocationEvent).toEqual(newerLocation);
const emitSpy = jest.spyOn(beacon, 'emit').mockClear();
+63 -1
View File
@@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "../../../src/models/event";
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils";
import { EventType } from "../../../src";
import { Crypto } from "../../../src/crypto";
describe('MatrixEvent', () => {
it('should create copies of itself', () => {
@@ -57,4 +60,63 @@ describe('MatrixEvent', () => {
expect(a.toSnapshot().isEquivalentTo(a)).toBe(true);
expect(a.toSnapshot().isEquivalentTo(b)).toBe(false);
});
it("should prune clearEvent when being redacted", () => {
const ev = new MatrixEvent({
type: "m.room.message",
content: {
body: "Test",
},
event_id: "$event1:server",
});
expect(ev.getContent().body).toBe("Test");
expect(ev.getWireContent().body).toBe("Test");
ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", "");
expect(ev.getContent().body).toBe("Test");
expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).toBe("xyz");
const redaction = new MatrixEvent({
type: "m.room.redaction",
redacts: ev.getId(),
});
ev.makeRedacted(redaction);
expect(ev.getContent().body).toBeUndefined();
expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).toBeUndefined();
});
it("should abort decryption if fails with an error other than a DecryptionError", async () => {
const ev = new MatrixEvent({
type: EventType.RoomMessageEncrypted,
content: {
body: "Test",
},
event_id: "$event1:server",
});
await ev.attemptDecryption({
decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")),
} as unknown as Crypto);
expect(ev.isEncrypted()).toBeTruthy();
expect(ev.isBeingDecrypted()).toBeFalsy();
expect(ev.isDecryptionFailure()).toBeFalsy();
});
describe("applyVisibilityEvent", () => {
it("should emit VisibilityChange if a change was made", async () => {
const ev = new MatrixEvent({
type: "m.room.message",
content: {
body: "Test",
},
event_id: "$event1:server",
});
const prom = emitPromise(ev, MatrixEventEvent.VisibilityChange);
ev.applyVisibilityEvent({ visible: false, eventId: ev.getId(), reason: null });
await prom;
});
});
});
+28
View File
@@ -0,0 +1,28 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Thread } from "../../../src/models/thread";
describe('Thread', () => {
describe("constructor", () => {
it("should explode for element-web#22141 logging", () => {
// Logging/debugging for https://github.com/vector-im/element-web/issues/22141
expect(() => {
new Thread("$event", undefined, {} as any); // deliberate cast to test error case
}).toThrow("element-web#22141: A thread requires a room in order to function");
});
});
});
+134
View File
@@ -0,0 +1,134 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Feature, ServerSupport } from "../../src/feature";
import {
EventType,
fixNotificationCountOnDecryption,
MatrixClient,
MatrixEvent,
MsgType,
NotificationCountType,
RelationType,
Room,
RoomEvent,
} from "../../src/matrix";
import { IActionsObject } from "../../src/pushprocessor";
import { ReEmitter } from "../../src/ReEmitter";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
import { mkEvent, mock } from "../test-utils/test-utils";
let mockClient: MatrixClient;
let room: Room;
let event: MatrixEvent;
let threadEvent: MatrixEvent;
const ROOM_ID = "!roomId:example.org";
let THREAD_ID;
function mkPushAction(notify, highlight): IActionsObject {
return {
notify,
tweaks: {
highlight,
},
};
}
describe("fixNotificationCountOnDecryption", () => {
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
getRoom: jest.fn().mockImplementation(() => room),
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
});
mockClient.reEmitter = mock(ReEmitter, 'ReEmitter');
mockClient.canSupport = new Map();
Object.keys(Feature).forEach(feature => {
mockClient.canSupport.set(feature as Feature, ServerSupport.Stable);
});
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "");
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
event = mkEvent({
type: EventType.RoomMessage,
content: {
msgtype: MsgType.Text,
body: "Hello world!",
},
event: true,
}, mockClient);
THREAD_ID = event.getId();
threadEvent = mkEvent({
type: EventType.RoomMessage,
content: {
"m.relates_to": {
rel_type: RelationType.Thread,
event_id: THREAD_ID,
},
"msgtype": MsgType.Text,
"body": "Thread reply",
},
event: true,
});
room.createThread(THREAD_ID, event, [threadEvent], false);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
});
it("changes the room count to highlight on decryption", () => {
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
it("changes the thread count to highlight on decryption", () => {
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
fixNotificationCountOnDecryption(mockClient, threadEvent);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
});
it("emits events", () => {
const cb = jest.fn();
room.on(RoomEvent.UnreadNotifications, cb);
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
expect(cb).toHaveBeenLastCalledWith({ highlight: 0, total: 1 });
room.setUnreadNotificationCount(NotificationCountType.Highlight, 5);
expect(cb).toHaveBeenLastCalledWith({ highlight: 5, total: 1 });
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 5);
expect(cb).toHaveBeenLastCalledWith({ highlight: 5 }, "$123");
});
});
+69
View File
@@ -0,0 +1,69 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import MockHttpBackend from 'matrix-mock-request';
import { MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
import { mkPusher } from '../test-utils/test-utils';
const realSetTimeout = setTimeout;
function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}
let client: MatrixClient;
let httpBackend: MockHttpBackend;
describe("Pushers", () => {
beforeEach(() => {
httpBackend = new MockHttpBackend();
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
});
describe("supports remotely toggling push notifications", () => {
it("migration support when connecting to a legacy homeserver", async () => {
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
unstable_features: {
"org.matrix.msc3881": false,
},
});
httpBackend.when("GET", "/pushers").respond(200, {
pushers: [
mkPusher(),
mkPusher({ [PUSHER_ENABLED.name]: true }),
mkPusher({ [PUSHER_ENABLED.name]: false }),
],
});
const promise = client.getPushers();
await httpBackend.flushAllExpected();
await flushPromises();
const response = await promise;
expect(response.pushers[0][PUSHER_ENABLED.name]).toBe(true);
expect(response.pushers[1][PUSHER_ENABLED.name]).toBe(true);
expect(response.pushers[2][PUSHER_ENABLED.name]).toBe(false);
});
});
});

Some files were not shown because too many files have changed in this diff Show More