Compare commits

..

325 Commits

Author SHA1 Message Date
RiotRobot a491508543 v28.2.0 2023-09-26 12:07:30 +01:00
RiotRobot 0abba3e626 Prepare changelog for v28.2.0 2023-09-26 12:07:26 +01:00
RiotRobot d669ddfab2 v28.2.0-rc.1 2023-09-19 12:34:58 +01:00
RiotRobot 9caa38d386 Prepare changelog for v28.2.0-rc.1 2023-09-19 12:34:55 +01:00
maheichyk 1c16b5cae6 Delete knocked room when knock membership changes (#3729)
* Store leave state when knock is denied

Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>

* Delete knocked room when knock request is cancelled or denied

Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>

* Test is updated

Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>

---------

Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
Co-authored-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
2023-09-18 17:08:53 +00:00
Richard van der Hoff cb375e1351 rust impl of getEncryptionInfoForEvent (#3718) 2023-09-18 14:49:24 +00:00
Richard van der Hoff 5e542b3869 Fix potential delay in sending out requests from the rust SDK (#3717)
* Emit a `UserTrustStatusChanged` when user identity is updated

* Remove redundant `onCrossSigningKeysImport` callback

This now happens as a side-effect of importing the keys.

* bump to alpha release of matrix-rust-sdk-crypto-wasm

* fixup! Remove redundant `onCrossSigningKeysImport` callback

* Fix potential delay in sending out requests from the rust SDK

There was a potential race which could cause us to be very slow to send out
pending HTTP requests, particularly when handling a user verification. Add some
resiliece to make sure we handle it correctly.

* add comments

* Add a unit test

---------

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2023-09-18 14:11:33 +00:00
Valere c9435af637 Cleanup, separate backup bootstrap and mgmt (#3726)
* Cleanup, separate backup bootstrap and mgmt

* fix non spec compliant account data format
2023-09-15 11:38:49 +00:00
Valere 40168d4419 Rust: Query backup on fail to decrypt similar to libolm (#3711)
* Refactor key backup recovery to prepare for rust

* rust backup restore support

* map decryption errors correctly from rust

* query backup on fail to decrypt
2023-09-14 10:10:53 +00:00
Valere 6d118008b6 Map decryption errors correctly from rust (#3710)
* Refactor key backup recovery to prepare for rust

* rust backup restore support

* map decryption errors correctly from rust

* Move export out of old crypto to api with re-export

* extract base64 utility

* add tests for base64 util

* more efficient regex

* fix typo

* use different vector for bob

* missing import

* Group tests for decryption errors

* Do not map unneeded rust error for now
2023-09-13 13:34:55 +00:00
Valere 1503acb30a rust backup restore support (#3709)
* Refactor key backup recovery to prepare for rust

* rust backup restore support

* Move export out of old crypto to api with re-export

* extract base64 utility

* add tests for base64 util

* more efficient regex

* fix typo
2023-09-13 09:08:26 +00:00
David Baker 1b8507c060 Merge pull request #3723 from matrix-org/dbkr/fix_codeowners
Fix codeowners
2023-09-13 10:05:03 +01:00
David Baker d95b5ab27a Fix codeowners
Accidental change from merging https://github.com/matrix-org/matrix-js-sdk/pull/3663
2023-09-13 09:53:19 +01:00
ElementRobot 658e7b1be3 Resetting package fields for development 2023-09-12 16:53:47 +01:00
ElementRobot 95110eb889 Merge branch 'master' into develop 2023-09-12 16:53:44 +01:00
ElementRobot 9fbcef556e v28.1.0 2023-09-12 16:52:42 +01:00
ElementRobot b68ad00394 Prepare changelog for v28.1.0 2023-09-12 16:52:40 +01:00
David Baker 6836720e1e Introduce MatrixRTCSession lower level group call primitive (#3663)
* Add hacky option to disable the actual calling part of group calls.

So we can try using livekit instead.

* Put LiveKit info into the `m.call` state event (#3522)

* Put LK info into state

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

* Update to the new way the LK service works

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

---------

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

* Send 'contentLoaded' event

As per comment, so we can start digging ourselves out of the widget
API hole we're currently in.

* Add comment on updating the livekit service URL

* Appease CI on `livekit` branch (#3566)

* Update codeowners on `livekit` branch (#3567)

* add getOpenIdToken to embedded client backend

Signed-off-by: Timo K <toger5@hotmail.de>

* add test and update comment

Signed-off-by: Timo K <toger5@hotmail.de>

* Merge `develop` into `livekit` (#3569)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: RiotRobot <releases@riot.im>
Co-authored-by: Florian Duros <florianduros@element.io>
Co-authored-by: Kerry <kerrya@element.io>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
Co-authored-by: Erik Johnston <erik@matrix.org>
Co-authored-by: Valere <bill.carson@valrsoft.com>
Co-authored-by: Hubert Chathi <hubertc@matrix.org>
Close IDB database before deleting it to prevent spurious unexpected close errors (#3478)
Fix export type `GeneratedSecretStorageKey` (#3479)
Fix order of things in `crypto-api.ts` (#3491)
Fix bug where switching media caused media in subsequent calls to fail (#3489)
fixes (#3515)
fix the integ tests, where #3509 etc fix the unit tests.
fix breakage on node 16 (#3527)
Fix an instance of failed to decrypt error when an in flight `/keys/query` fails. (#3486)
Fix `TypedEventEmitter::removeAllListeners(void)` not working (#3561)

* Revert "Merge `develop` into `livekit`" (#3572)

* Don't update calls with no livekit URL & expose method to update it instead

and generally simplify a bit: change it to a single string rather than
an array of structs.

* Fix other instances of passing focusInfo / livekit url

* Add temporary setter

* WIP refactor for removing m.call events

* Always remember rtcsessions since we need to only have one instance

* Fix tests

* Fix import loop

* Fix more cyclic imports & tests

* Test session joining

* Attempt to make tests happy

* Always leave calls in the tests to clean up

* comment + desperate attempt to work out what's failing

* More test debugging

* Okay, so these ones are fine?

* Stop more timers and hopefully have happy tests

* Test no rejoin

* Test malformed m.call.member events

* Test event emitting

and also move some code to a more sensible place in the file

* Test getActiveFoci()

* Test event emitting (and also fix it)

* Test membership updating & pruning on join

* Test getOldestMembership()

* Test member event renewal

* Don't start the rtc manager until the client has synced

Then we can initialise from the state once it's completed.

* Fix type

* Remove listeners added in constructor

* Stop the client here too

* Stop the client here also also

* ARGH. Disable tests to work out which one is causing the exception

* Disable everything

* Re-jig to avoid setting listeners in the constructor

and re-enable tests

* No need to rename this anymore

* argh, remove the right listener

* Is it this test???

* Re-enable some tests

* Try mocking getRooms to return something valid

* Re-enable other tests

* Give up trying to get the tests to work sensibly and deal with getRooms() returning nothing

* Oops, don't enable the ones that were skipped before

* One more try at the sensible way

* Didn't work, go back to the hack way.

* Log when we manage to send the member event update

* Support `getOpenIdToken()` in embedded mode (#3676)

* Call `sendContentLoaded()` (#3677)

* Start MatrixRTC in embedded mode (#3679)

* Reschedule the membership event check

* Bump widget api version

* Add mock for sendContentLoaded()

* More log detail

* Fix tests

and also better assert because the tests were passing undefined which
was considered fine because we were only checking for null.

* Simplify updateCallMembershipEvent a bit

* Split up updateCallMembershipEvent some more

* Typo

Co-authored-by: Daniel Abramov <inetcrack2@gmail.com>

* Expand comment

* Add comment

* More comments

* Better comment

* Sesson

* Rename some variables

* Comment

* Remove unused method

* Wrap updatecallMembershipEvent so it only runs one at a time

* Do another update if another one is triggered while the update happens

* Make triggerCallMembershipEventUpdate async

* Fix test & some missed timer removals

* Mark session manager as unstable

---------

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
Co-authored-by: Timo K <toger5@hotmail.de>
Co-authored-by: Timo <16718859+toger5@users.noreply.github.com>
Co-authored-by: Daniel Abramov <inetcrack2@gmail.com>
2023-09-12 15:08:15 +00:00
renovate[bot] 6f517478df Update jest to v29.5.4 (#3670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 14:50:39 +00:00
Andy Balaam 35ba4074de Avoid an infinite loop in the interactive-auth test (#3722)
Reading the test "should fire stateUpdated callback with error when a
request fails" it looks like this would always cause an infinite loop
between doRequest and statusUpdated calls. I don't know why this wasn't
a problem until we updated Jest to v29.5.4, but after that point it was.

This change should fix the test failures for the Jest upgrade PR:
https://github.com/matrix-org/matrix-js-sdk/pull/3670 .
2023-09-12 14:03:47 +00:00
Valere c7827d971c Refactor key backup recovery to prepare for rust (#3708)
* Refactor key backup recovery to prepare for rust

* code review

* quick doc format

* code review fix
2023-09-12 11:19:35 +00:00
Richard van der Hoff f963ca5562 Element-R: Emit CryptoEvent.UserTrustStatusChanged when user identity is updated (#3716)
* Emit a `UserTrustStatusChanged` when user identity is updated

* Remove redundant `onCrossSigningKeysImport` callback

This now happens as a side-effect of importing the keys.

* bump to alpha release of matrix-rust-sdk-crypto-wasm

* fixup! Remove redundant `onCrossSigningKeysImport` callback
2023-09-08 04:40:02 +00:00
Malte Finsterwalder 8c30b0d12c Dont access indexed db when undefined (#3707)
* T-Defect: handle undefined indexedDB gracefully

Signed-off-by: Malte Finsterwalder <malte@holi.team>

* T-Defect: test to check handling of undefined indexedDB gracefully

Signed-off-by: Malte Finsterwalder <malte@holi.team>

---------

Signed-off-by: Malte Finsterwalder <malte@holi.team>
Co-authored-by: Malte Finsterwalder <malte@holi.team>
2023-09-07 21:52:32 +00:00
Andy Balaam 5d4334ba4c Explain why synthetic receipts don't mark the room as read (#3715)
* Explain why synthetic receipts don't mark the room as read

* Fix misleading "local echo" comment with "remote echo"

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-07 15:31:54 +00:00
Richard van der Hoff 7e691bf700 Implement getEncryptionInfoForEvent and deprecate getEventEncryptionInfo (#3693)
* Implement `getEncryptionInfoForEvent` and deprecate `getEventEncryptionInfo`

* fix tsdoc

* fix tests

* Improve test coverage
2023-09-07 09:39:10 +00:00
Andy Balaam 0700e86f58 Don't reset unread count when adding a synthetic receipt (#3706)
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/3684
and there are lots more details about why we chose this solution in that
issue.
2023-09-07 07:24:51 +00:00
maheichyk 6c307d4c63 Sync knock rooms (#3703)
Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
Co-authored-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
2023-09-06 17:10:14 +00:00
renovate[bot] 88ec0e3e17 Update babel monorepo to v7.22.15 (#3704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 12:56:01 +00:00
renovate[bot] 015e9a5be7 Update all non-major dependencies (#3700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 12:42:44 +00:00
renovate[bot] 2918d686ae Update definitelyTyped (#3698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 09:04:49 +00:00
renovate[bot] 327c18ddc1 Update babel monorepo (#3697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 09:04:32 +00:00
renovate[bot] 8cdd8e882b Update peter-evans/repository-dispatch digest to bf47d10 (#3696)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 09:02:54 +00:00
renovate[bot] 76e0d5a896 Update actions/checkout action to v4 (#3701)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-05 17:30:43 +00:00
ElementRobot 836238c3ba v28.1.0-rc.1 2023-09-05 16:51:26 +01:00
ElementRobot 014b29b303 Prepare changelog for v28.1.0-rc.1 2023-09-05 16:51:23 +01:00
Richard van der Hoff 74160806c0 Deprecate MatrixClient.checkUserTrust (#3691)
* New `CryptoApi.getUserVerificationStatus` API

* Add `RustCrypto#getUserVerificationStatus` tests

---------

Co-authored-by: Florian Duros <florianduros@element.io>
2023-09-05 14:58:10 +00:00
Michael Telatynski 8e0ef98bcc Provide better error for ICE Server SyntaxError (#3694)
* Provide better error for ICE Server SyntaxError

* Refactor

* Add test
2023-09-05 14:18:30 +00:00
Valere d7831f9e5b Implement key backup APIs for rust and create backup in bootstrapSecretStorage (#3690)
* new resetKeyBackup API

* add delete backup version test

* code review

* support backup creation in rust

* code review
2023-09-05 13:52:49 +00:00
Michael Telatynski 989c5a3dda Allow calls without ICE/TURN/STUN servers (#3695) 2023-09-05 12:44:39 +00:00
Richard van der Hoff 0778c4e01e Re-check key backup after bootstrapSecretStorage (#3692)
Fixes https://github.com/vector-im/element-web/issues/26115
2023-09-05 09:10:58 +00:00
Valere c65e329101 Deprecate MatrixClient.{prepare,create}KeyBackupVersion in favour of new CryptoApi.resetKeyBackup API (#3689)
* new resetKeyBackup API

* add delete backup version test

* code review

* code review
2023-09-04 20:00:28 +00:00
Germain 5ddd453699 Emit summary update event (#3687)
* Emit summary update event

* Add documentation

* Update RoomSummary event documentation

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-08-31 08:48:16 +00:00
Richard van der Hoff 42d982dd69 OutgoingRequestProcessor: do not throw errors if shutting down (#3683)
* `OutgoingRequestProcessor`: do not throw errors if shutting down

* Optimised builds throw a different error
2023-08-30 14:56:06 +00:00
Richard van der Hoff f406ffd3dd RustCrypto.getCrossSigningStatus: check the client is not stopped (#3682)
* `RustCrypto.getCrossSigningStatus`: check the client is not stopped

Better error handling for the case that a call to `MatrixClient.stop` happens
while the call to `getCrossSigningStatus` (or `isCrossSigningReady`) is in
flight.

* fix up tsdoc
2023-08-30 09:30:31 +00:00
Florian Duros dec4650d3d ElementR: Update CryptoApi.userHasCrossSigningKeys (#3646)
* WIP `CryptoApi.getStoredCrossSigningForUser`

* Fix QRCode

* Add docs and rename

* Add tests for `RustCrossSigningInfo.ts`

* Do `/keys/query` instead of using `UserIdentity`

* Review changes

* Get rid of `CrossSigningInfo`

* Merge `hasCrossSigningKeysForUser` into `userHasCrossSigningKeys`

* Apply suggestions from code review

* More review comments

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2023-08-29 11:27:28 +00:00
RiotRobot 4c00b41046 Resetting package fields for development 2023-08-29 10:55:15 +01:00
RiotRobot a1845ba0ff Merge branch 'master' into develop 2023-08-29 10:55:09 +01:00
RiotRobot fb9e258468 v28.0.0 2023-08-29 10:53:33 +01:00
RiotRobot 974723ceef Prepare changelog for v28.0.0 2023-08-29 10:53:30 +01:00
renovate[bot] 5788d9744b Update all non-major dependencies (#3671)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-24 13:01:48 +00:00
Johannes Marbach 65cbbaaf01 Use sender instead of content.creator field on m.room.create events (#3675)
* Use sender instead of content.creator field on m.room.create events

* Restore room_version fields in fixtures

* Add test case for undefined sender scenario
2023-08-24 11:58:04 +00:00
renovate[bot] c5245a887b Update dependency @types/node to v18.17.6 (#3669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 13:47:35 +00:00
Charly Nguyen 321679fd63 Add join_rule field to /publicRooms response (#3673)
Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
2023-08-23 13:35:43 +00:00
Michael Weimann 15c679b29e Improve/fix restoreKeyBackupWithPassword docs (#3674) 2023-08-23 12:58:10 +00:00
Michael Telatynski 85ba069117 Export de-facto public types out of @types (#3666)
* Export de-facto public types out of @types

* Make typedoc happier
2023-08-22 15:47:22 +00:00
RiotRobot 9b8dcf53ed v28.0.0-rc.1 2023-08-22 15:18:45 +01:00
RiotRobot 324af3ee67 Prepare changelog for v28.0.0-rc.1 2023-08-22 15:18:42 +01:00
RiotRobot ec6c0946d4 v27.3.0-rc.1 2023-08-22 15:13:46 +01:00
RiotRobot e5f480b032 Prepare changelog for v27.3.0-rc.1 2023-08-22 15:13:43 +01:00
Florian Duros 6bf4ed8672 ElementR: Add CryptoApi.requestVerificationDM (#3643)
* Add `CryptoApi.requestVerificationDM`

* Fix RoomMessageRequest url

* Review changes

* Merge fixes

* Add BOB test data

* `requestVerificationDM` test works against old crypto (encrypted verification request)

* Update test data
2023-08-21 14:48:32 +00:00
Valere c18d691ef5 RustCrypto | Implement keybackup loop (#3652)
* Implement `CryptoApi.checkKeyBackup`

* Deprecate `MatrixClient.enableKeyBackup`.

* fix integ test

* more tests

* Implement keybackup loop

* cleaning

* update matrix-sdk-crypto-wasm to 1.2.1

* fix lint

* avoid real timer stuff

* Simplify test

* post merge lint fix

* revert change on yarn.lock

* code review

* Generate test data for exported keys

* code review cleaning

* cleanup legacy backup loop

* Update spec/test-utils/test-data/generate-test-data.py

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update spec/test-utils/test-data/generate-test-data.py

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* update yarn.lock for new wasm bindings

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-08-17 14:10:57 +00:00
ElementRobot 97cf73bc52 Resetting package fields for development 2023-08-15 13:19:02 +01:00
ElementRobot aa25103665 Merge branch 'master' into develop 2023-08-15 13:18:59 +01:00
ElementRobot 858db67778 v27.2.0 2023-08-15 13:17:57 +01:00
ElementRobot e230abee45 Prepare changelog for v27.2.0 2023-08-15 13:17:55 +01:00
Travis Ralston 8c16d69f3c Set minimum supported Matrix 1.1 version (drop legacy r0 versions) (#3007)
Co-authored-by: Germain <germains@element.io>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-08-14 09:25:15 +01:00
Michael Telatynski 55b9116c99 Re-export localStorage-crypto-store (#3660) 2023-08-11 06:02:52 +00:00
Florian Duros 3a5d66057e ElementR: Process all verification events, not just requests (#3650)
* Process all verification event

* Add test for `isVerificationEvent`

* Review changes

* Remove null comparison and add doc to remote echo

* review changes
2023-08-09 14:14:58 +00:00
Richard van der Hoff 3f7af189e4 Implement CryptoApi.checkKeyBackupAndEnable (#3633)
* Implement `CryptoApi.checkKeyBackup`

* Deprecate `MatrixClient.enableKeyBackup`.

* fix integ test

* more tests

---------

Co-authored-by: valere <valeref@matrix.org>
2023-08-09 09:59:03 +00:00
renovate[bot] 16ddcb0ed0 Lock file maintenance (#3659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 15:32:57 +00:00
renovate[bot] 9e35b8dd0a Update all non-major dependencies (#3656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 15:09:57 +00:00
renovate[bot] bed787b749 Update tibdex/backport digest to 9565281 (#3658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 14:14:28 +00:00
renovate[bot] d260b8be56 Update dependency eslint-config-prettier to v9 (#3657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 14:10:12 +00:00
renovate[bot] 97991dad02 Update dependency @types/node to v18.17.3 (#3655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 14:09:52 +00:00
renovate[bot] b8c19c47ab Update babel monorepo to v7.22.10 (#3654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 14:07:45 +00:00
ElementRobot 1476ffbd15 v27.2.0-rc.1 2023-08-08 15:01:44 +01:00
ElementRobot 62f0a65472 Prepare changelog for v27.2.0-rc.1 2023-08-08 15:01:41 +01:00
Charly Nguyen 2ef7ae7661 Allow knocking rooms (#3647)
Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
2023-08-03 08:16:18 +00:00
Michael Telatynski 61c0a49971 Mark more logs as debug to tidy the console (#3645) 2023-08-02 20:40:32 +00:00
Michael Telatynski 2172f28888 Fix wrong handling of encrypted rooms when loading them from sync accumulator (#3640)
* Revert "Ensure we don't overinflate the total notification count (#3634)"

This reverts commit fd0c4a7f56.

* Fix wrong handling of encrypted rooms when loading them from sync accumulator

* Tidy up code, removing sections which didn't make any difference

* Add test
2023-08-02 09:53:34 +00:00
Florian Duros 2e9b34e0c3 Throw error if missing userId in CryptoApi.findVerificationRequestDMInProgress (#3641) 2023-08-01 12:20:01 +00:00
ElementRobot 5a782b7377 Resetting package fields for development 2023-08-01 12:28:37 +01:00
ElementRobot 54bc807056 Merge branch 'master' into develop 2023-08-01 12:28:33 +01:00
ElementRobot 9e07710d80 v27.1.0 2023-08-01 12:27:34 +01:00
ElementRobot e9ed91d800 Prepare changelog for v27.1.0 2023-08-01 12:27:31 +01:00
Michael Telatynski 88ba4fad71 Skip processing thread roots and fetching threads list when support is disabled (#3642)
* Skip processing thread roots and fetching threads list when support is disabled

* Enable threads support in tests
2023-07-31 18:16:42 +00:00
Michael Telatynski 21b3471453 Bump pagination limit to account for threaded events (#3638) 2023-07-31 16:59:55 +00:00
Florian Duros 0ada9803ab ElementR: Add CryptoApi.findVerificationRequestDMInProgress (#3601)
* Add `CryptoApi.findVerificationRequestDMInProgress`

* Fix linting and missing parameters

* Move `ROOM_ID` into `test-data`

* Remove verification request from `EventDecryptor` pending list

* Fix duplicate timeline event processing

* Add extra documentation

* Try to fix sonar error

* Use `roomId`

* Fix typo

* Review changes

* Review changes

* Fix `initRustCrypto` jsdoc

* Listen to `ClientEvent.Event` instead of `RoomEvent.Timeline`

* Fix missing room id in `generate-test-data.py`

* Review changes

* Review changes

* Handle encrypted event

* Fix linting

* Comments and run timers

* Ignore 404

* Fix test
2023-07-31 15:00:15 +00:00
Michael Telatynski 1744f0e97b Revert "Ensure we don't overinflate the total notification count (#3634)" (#3639) 2023-07-31 11:57:11 +01:00
Michael Telatynski fd0c4a7f56 Ensure we don't overinflate the total notification count (#3634)
* Ensure we don't overinflate the total notification count

By correctly comparing push rules before & after decryption

* DRY the code

* Testsssss

* Update tests
2023-07-28 15:05:11 +00:00
Michael Telatynski 615f7f9e72 Export more into the public interface (#3614) 2023-07-28 11:54:17 +00:00
renovate[bot] 77259e81c9 Update all non-major dependencies (#3630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-28 10:55:22 +00:00
Richard van der Hoff 2193cd9d1c Implement CryptoApi.isKeyBackupTrusted (#3632)
* Implement `CryptoApi.isKeyBackupTrusted`

Fixes https://github.com/vector-im/crypto-internal/issues/110

* Bump matrix-sdk-crypto-wasm to v1.2.0

* Back out some changes

These are unneeded, and break backwards compat
2023-07-28 09:54:55 +00:00
Valere 6d28154dcd Add CryptoApi.getActiveSessionBackupVersion() (#3555)
* stub backupmanager

* Implement `CryptoApi.getActiveSessionBackupVersion`

* Revert unnecessary change

we can do this later, once we have better test coverage

* more test coverage

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
2023-07-28 08:04:20 +00:00
Richard van der Hoff 83d447adfe Clean up megolm-backup integ test (#3631)
* Add `CryptoApi.setDeviceVerified`

I need a way to mark devices as trusted for the backup tests.

* More tests

* Simplify E2EKeyResponder.addDeviceKeys

The user and device IDs are in the test data, so no need to pass them in

* Clean up key backup integration test

Make it use the CryptoApi rather than legacy `MatrixClient.crypto`, and use a
pre-signed backup instead of requiring a "blindlySignAnything" method.

* run megolm-backup tests on both crypto stacks

* avoid internal backupManager
2023-07-27 15:23:02 +00:00
Richard van der Hoff 73c9f4e322 Add CryptoApi.setDeviceVerified (#3624)
I need a way to mark devices as trusted for the backup tests.
2023-07-27 13:16:10 +01:00
renovate[bot] e6fa4cdb3c Lock file maintenance (#3629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-27 09:45:53 +00:00
Michael Telatynski a04653a72c Upgrade matrix-widget-api (#3621)
* Fix threads ending up with chunks of their timelines missing

* delint

* Upgrade matrix-widget-api
2023-07-27 09:21:14 +00:00
renovate[bot] 5f9341f39c Update dependency eslint-plugin-unicorn to v48 (#3628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-27 08:37:46 +00:00
renovate[bot] 906946c419 Update all non-major dependencies (#3626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-27 08:37:23 +00:00
renovate[bot] 4397b9d640 Update dependency @types/node to v18.17.0 (#3627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-27 08:37:02 +00:00
ElementRobot 90da2cf439 v27.1.0-rc.1 2023-07-27 09:28:56 +01:00
ElementRobot 6edd45787b Prepare changelog for v27.1.0-rc.1 2023-07-27 09:28:52 +01:00
Florian Duros 84444ec11e ElementR: Add CryptoApi.getCrossSigningKeyId (#3619)
* Add `CryptoApi.getCrossSigningKeyId`

* Rename `CrossSigningPubKey` to `CrossSigningKeyInfo`

* Remove old eslint disable

* Review changes

* Review changes
2023-07-26 16:09:49 +00:00
Richard van der Hoff 0e95df5dba Element-R: implement {get,store}SessionBackupPrivateKey (#3622) 2023-07-26 12:00:43 +01:00
Valere 29b815b678 Replace deprecated TestClient with fetchMock (#3550)
* replace deprecated TestClient with fetchMock

* add stop() api to BackupManager for clean shutdown

* fix merge

* code review cleaning

* lint

* Address review comments

* Remove unused `TestClient.expectKeyBackupQuery`

* clean up imports

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-07-25 20:42:41 +00:00
Richard van der Hoff 0cf056958b Fix broken unit tests for FetchHttpApi.getUrl (#3620)
These tests have broken on Node.js 18.17.0.

This is due to Node.js adopting an updated version of the URL parser, in which
the internal `Symbol(query)` property is populated lazily.

We shouldn't be relying on the internal state of the URL object anyway. Let's
just compare the stringified copy.
2023-07-25 19:48:15 +00:00
Florian Duros 79d4113a6b ElementR: Stub CheckOwnCrossSigningTrust, import cross signing keys and verify local device in bootstrapCrossSigning (#3608) 2023-07-25 18:03:43 +01:00
Michael Telatynski 8a80886358 Fix threads ending up with chunks of their timelines missing (#3618)
* Fix threads ending up with chunks of their timelines missing

* delint
2023-07-25 15:28:52 +00:00
Michael Telatynski de7959de6c Ensure we do not clobber a newer RR with an older unthreaded one (#3617)
* Ensure we do not clobber a newer RR with an older unthreaded one

or vice versa

* Fix test
2023-07-24 16:35:56 +00:00
Michael Telatynski 533c21a515 Fix registration check your emails stage regression (#3616)
* Fix registration check your emails stage regression

* Simplify diff

* Add test
2023-07-24 14:08:17 +00:00
Michael Telatynski 6b018b6927 Fix how Room::eventShouldLiveIn handles replies to unknown parents (#3615)
* Add warning

* Fix how Room::eventShouldLiveIn handles replies to unknown parents
2023-07-24 07:37:28 +00:00
Michael Telatynski 38c3abb364 Update downstream-artifacts.yml (#3613) 2023-07-20 21:43:49 +00:00
Michael Telatynski a47f319665 Only send threaded read receipts if threads support is enabled (#3612)
* Only send threaded read receipts if threads support is enabled

* Tests
2023-07-20 15:44:52 +00:00
Florian Duros ecef9fd755 Fix CryptoApi#getVerificationRequestsToDeviceInProgress (#3611) 2023-07-20 09:46:55 +00:00
Richard van der Hoff 7dffd8ffd3 Make sure to drop references to user device lists (#3610)
Empirically, this seems to fix some problems with leaking references to
IndexedDB.
2023-07-20 08:47:30 +00:00
Michael Telatynski 66492e7ba8 Fix edge cases around non-thread relations to thread roots and read receipts (#3607)
* Ensure non-thread relations to a thread root are actually in both timelines

* Make thread in sendReceipt & sendReadReceipt explicit rather than guessing it

* Apply suggestions from code review

* Fix Room::eventShouldLiveIn to better match Synapse to diverging ideas of notifications

* Update read receipt sending behaviour to align with Synapse

* Fix tests

* Fix thread rel type
2023-07-19 11:21:50 +00:00
Michael Telatynski 43b2404865 Specify /preview_url requests as low priority (#3609)
* Specify /preview_url requests as low priority

* Update src/@types/global.d.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Simplify interface

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-07-19 09:29:41 +00:00
Michael Telatynski fed9910fa1 Remove unused better-docs & docdash (#3605) 2023-07-19 07:44:11 +00:00
RiotRobot f77662406c Resetting package fields for development 2023-07-18 12:53:43 +01:00
RiotRobot 8cc0cf1a70 Merge branch 'master' into develop 2023-07-18 12:53:38 +01:00
RiotRobot dfa2429094 v27.0.0 2023-07-18 12:52:00 +01:00
RiotRobot 3e2460707c Prepare changelog for v27.0.0 2023-07-18 12:51:57 +01:00
Michael Telatynski 706c084fa7 Add tests for room-hierarchy (#3606)
* Add tests for room-hierarchy

* overwriteroutes
2023-07-18 10:37:17 +00:00
renovate[bot] eb7faa6c07 Lock file maintenance (#3604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-17 17:59:37 +00:00
RiotRobot d45a0b894a v27.0.0-rc.2 2023-07-14 16:10:12 +01:00
RiotRobot 102739e0fb Prepare changelog for v27.0.0-rc.2 2023-07-14 16:10:09 +01:00
Andy Balaam 0d7e4a0fa5 Merge pull request #3603 from matrix-org/backport-3600-to-staging
[Backport staging] Fix read receipt sending behaviour around thread roots
2023-07-14 16:03:41 +01:00
Michael Telatynski d4628e78d4 Fix read receipt sending behaviour around thread roots (#3600)
* Fix read receipt sending behaviour around thread roots

* Update src/client.ts

Co-authored-by: Eric Eastwood <erice@element.io>

---------

Co-authored-by: Eric Eastwood <erice@element.io>
(cherry picked from commit b05f933d83)
2023-07-14 15:01:58 +00:00
Richard van der Hoff 0b193f4665 matrix-sdk-crypto-js -> matrix-sdk-crypto-wasm (#3602)
We've renamed matrix-sdk-crypto-js and released a v1.0.
2023-07-13 17:11:57 +00:00
Eric Eastwood 8ef2e848b9 Log query parameters on HTTP requests (#3591)
* Log query parameters on HTTP requests

Follow-up to https://github.com/matrix-org/matrix-js-sdk/pull/3485

* Only stringify once

See https://github.com/matrix-org/matrix-js-sdk/pull/3591#discussion_r1261300323
2023-07-13 13:07:01 +00:00
Richard van der Hoff d92936fba5 Element-R: support for displaying QR codes during verification (#3588)
* Support for showing QR codes

* Emit `VerificationRequestEvent.Change` events when the verifier changes

* Minor integ test tweaks

* Handle transitions from QR code display to SAS

* Fix naming

* Add a test for `ShowQrCodeCallbacks.cancel`
2023-07-13 11:11:13 +00:00
Michael Telatynski f005984df3 Export typed event emitter key types (#3597)
* Export typed event emitter key types

* Update src/matrix.ts
2023-07-13 11:10:24 +00:00
Richard van der Hoff 13fec49e74 Element-R: ensure that userHasCrossSigningKeys uses up-to-date data (#3599)
* Element-R: ensure that `userHasCrossSigningKeys` uses up-to-date data

* Bump matrix-sdk-crypto-js
2023-07-13 10:46:56 +00:00
renovate[bot] 008294cfc6 Update babel monorepo to v7.22.9 (#3434)
* Update babel monorepo to v7.22.9

* Make babel happier

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-07-13 09:41:03 +00:00
Michael Telatynski b05f933d83 Fix read receipt sending behaviour around thread roots (#3600)
* Fix read receipt sending behaviour around thread roots

* Update src/client.ts

Co-authored-by: Eric Eastwood <erice@element.io>

---------

Co-authored-by: Eric Eastwood <erice@element.io>
2023-07-13 08:25:23 +00:00
Michael Telatynski b186d79dde Fix jest/valid-expects lints (#3586) 2023-07-12 17:11:52 +00:00
Michael Telatynski e82b5fe1db Fix types in getSessionBackupPrivateKey (#3595)
* Fix type issue around `getSessionBackupPrivateKey`

* Fix sending auth: null due to broken types around UIA

* Discard changes to src/crypto/index.ts

* Add comment

* Fix types

* Fix types for MatrixClient::addThreePid

* Iterate
2023-07-12 14:38:14 +00:00
Michael Telatynski 9602aa88ea Fix sending auth: null due to broken types around UIA (#3594)
* Fix type issue around `getSessionBackupPrivateKey`

* Fix sending auth: null due to broken types around UIA

* Discard changes to src/crypto/index.ts

* Add comment
2023-07-12 13:55:02 +00:00
renovate[bot] 0fb3dc1b13 Update typescript-eslint monorepo to v5.62.0 (#3583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 11:01:00 +00:00
renovate[bot] aeede332be Update dependency @types/jest to v29.5.3 (#3582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 10:59:05 +00:00
renovate[bot] b052950a19 Update JS-DevTools/npm-publish action to v2.2.1 (#3581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 10:57:45 +00:00
Michael Telatynski 1cb5fff5a1 Improve types (#3589)
* Improve types

* Improve coverage
2023-07-12 10:39:33 +00:00
Michael Telatynski 01226e41d9 Fix broken DeviceList.spec.ts test (#3590) 2023-07-12 10:01:57 +00:00
dependabot[bot] e3919fd93b Bump semver from 5.7.1 to 5.7.2 (#3575)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kerry <kerrya@element.io>
2023-07-12 02:20:47 +00:00
Kerry 3b88ea19b7 Stabilize support for MSC3952: intentional mentions (#3397)
* use stable identifiers for MSC3952: intentional mentions

* add matrix version to feature support for intentional mentions
2023-07-11 22:04:06 +00:00
Richard van der Hoff dcf26f3e48 bump rust-sdk (#3587) 2023-07-11 17:40:39 +00:00
Valere 3385adf5f6 Improve logging of http requests to aid debugging (#3485)
* Simple request logging with status and duration

* remove url params from logs

* superfluous toString()

* Add tests

* Apply suggestions from code review

* update snapshots

* update log format

* Apply suggestions from code review

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* update snapshot

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2023-07-11 17:27:42 +00:00
Richard van der Hoff 9db6ce107a Add support for scanning QR codes during verification, with Rust crypto (#3565)
* Offer `m.qr_code.scan.v1` verification method by default

Normally, the application specifies the supported verification methods when
creating the MatrixClient (and matrix-react-sdk does so). If the application
leaves it unset, then the idea is that the js-sdk offers all known verification
methods.

However, by default, the rust-sdk doesn't specify `m.qr_code.scan.v1`. So
basically, we need to set our own list of supported methods, rather than
relying on the rust-sdk's defaults.

* Factor out base class from `RustSASVerifier`

* Implement QR code scanning

* Update src/rust-crypto/verification.ts
2023-07-11 16:00:59 +00:00
Florian Duros d5b22e1deb Use cryptoBackend in client.ts for new rust-crypto implementation (#3576)
* Use `cryptoBackend` in `client.ts` for new rust-crypto implementation for backward compatibility

* Use `cryptoBackend` in `client.ts` for new rust-crypto implementation for backward compatibility
2023-07-11 14:13:53 +00:00
Richard van der Hoff a5e606a1e7 Mark all the rust crypto stuff internal (#3574)
... for the avoidance of doubt.
2023-07-11 14:11:35 +00:00
Michael Telatynski f2471b6dbd Add methods to influence set_presence on /sync API calls (#3578)
* Add methods to influence set_presence on /sync API calls

* Tweak comment

* Improve coverage
2023-07-11 13:31:12 +00:00
RiotRobot dcf71e0c8f v27.0.0-rc.1 2023-07-11 13:37:19 +01:00
RiotRobot 77267e393c Prepare changelog for v27.0.0-rc.1 2023-07-11 13:37:16 +01:00
Michael Telatynski 1fdc0af5b7 Throw saner error when peeking has its room pulled out from under it (#3577) 2023-07-11 10:24:57 +00:00
Michael Telatynski d2b782a2f5 Simplify MatrixClient::setPowerLevel API (#3570)
* Simplify `MatrixClient::setPowerLevel` API

While making it more resilient to causing issues like nuking room state

* Handle edge case

* Fix tests

* Add test coverage
2023-07-11 07:26:30 +00:00
Kerry 5df4ebaada OIDC: Log in (#3554)
* use oidc-client-ts during oidc discovery

* export new type for auth config

* deprecate generateAuthorizationUrl in favour of generateOidcAuthorizationUrl

* testing util for oidc configurations

* test generateOidcAuthorizationUrl

* lint

* test discovery

* dont pass whole client wellknown to oidc validation funcs

* add nonce

* use oidc-client-ts for oidc response

* validate user state and update tests

* use oidc-client-ts for code exchange

* use oidc-client-ts in completing auth grant

* use client userState for homeserver

* more comments
2023-07-11 02:20:19 +00:00
renovate[bot] e68a1471c1 Update all non-major dependencies (#3564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-10 14:38:14 +00:00
Richard van der Hoff e42dd74426 Add async method for generating a QR code (#3562)
The api to generate a QR code is async in rust, and the easiest way to deal
with it is to make a new method.
2023-07-10 14:22:10 +01:00
renovate[bot] 2751e191d3 Lock file maintenance (#3392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-10 10:08:16 +00:00
Michael Telatynski b5b86bf1b5 Fix TypedEventEmitter::removeAllListeners(void) not working (#3561) 2023-07-10 10:04:38 +00:00
dependabot[bot] 4990bf5ca0 Bump tough-cookie from 4.1.2 to 4.1.3 (#3560)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 08:24:08 +00:00
Kerry b8fa030d5d OIDC: use oidc-client-ts (#3544)
* use oidc-client-ts during oidc discovery

* export new type for auth config

* deprecate generateAuthorizationUrl in favour of generateOidcAuthorizationUrl

* testing util for oidc configurations

* test generateOidcAuthorizationUrl

* lint

* test discovery

* dont pass whole client wellknown to oidc validation funcs

* add nonce

* use client userState for homeserver
2023-07-09 21:19:32 +00:00
Hubert Chathi b606d1e54b Don't allow Olm unwedging rate-limiting to race (#3549)
* don't allow Olm unwedging rate-limiting to race

* apply changes from code review
2023-07-07 10:43:25 +00:00
Michael Telatynski cd7c519dc4 Prevent threads code from making identical simultaneous API hits (#3541) 2023-07-07 09:48:09 +01:00
Michael Telatynski 30dd28960c Update IUnsigned type to be extensible (#3547) 2023-07-07 07:43:17 +00:00
Valere 5b635df08d add stop() api to BackupManager for clean shutdown (#3553) 2023-07-06 16:43:47 +00:00
Florian Duros 592c497902 Upgrade @matrix-org/matrix-sdk-crypto-js to ^0.1.1 (#3552) 2023-07-06 16:00:36 +00:00
Richard van der Hoff 8e3f2f3262 Log message ID for undecryptable to-device messages (#3543)
... to help with debugging.
2023-07-06 06:09:39 +00:00
ElementRobot 5751df1288 Resetting package fields for development 2023-07-04 15:08:16 +01:00
ElementRobot 40a71101e2 Merge branch 'master' into develop 2023-07-04 15:08:12 +01:00
ElementRobot 3f095caf2d v26.2.0 2023-07-04 15:07:04 +01:00
ElementRobot 12a94bdd94 Prepare changelog for v26.2.0 2023-07-04 15:07:02 +01:00
Michael Telatynski 1c1ac137d3 Improve types around login, registration, UIA and identity servers (#3537) 2023-07-04 14:49:24 +01:00
Michael Telatynski 89cabc4912 Ignore thread relations on state events for consistency with edits (#3540)
* Ignore thread relations on state events for consistency with edits

* Add test
2023-07-04 12:07:49 +00:00
Erik Johnston 5be4548b3d Fix an instance of failed to decrypt error when an in flight /keys/query fails. (#3486)
* Fix an instance of failed to decrypt error

Specifically, when checking the event sender matches who sent us the
session keys we skip waiting for pending device list updates if we
already know who owns the session key.

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/crypto/algorithms/olm.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Fix line wrapping

* Update src/crypto/algorithms/olm.ts

* Fix null check

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-07-04 11:31:03 +00:00
Kerry 09de76bd43 OIDC: validate id token (#3531)
* validate id token

* comments

* tidy comments
2023-07-03 21:12:15 +00:00
Richard van der Hoff 3a694f4998 Element-R: Implement VerificationRequest.{timeout,pending} (#3532)
* implement `VerificationRequest.pending`

* Implement `VerificationRequest.timeout`

* Rust crypto: allow using a memory store (#3536)

* Rust crypto: allow using a memory store

It turns out that, for some usecases (in particular, "bot users" for cypress
tests), we don't need persistent storage and an in-memory store will be fine.

* Rust crypto: use a memory store for the unit tests
2023-07-03 11:27:38 +00:00
Richard van der Hoff 3a8a1389f5 Element-R: Implement VerificationRequest.accept (#3526)
* Pass `supportedVerificationMethods` into `VerificationRequest`

... so that the application can later call `accept()` and we know what to send.

* Implement `VerificationRequest.accept`

* Implement `VerificationRequest.declining`

* Update src/rust-crypto/verification.ts
2023-07-03 11:02:19 +00:00
Richard van der Hoff c271e1533a Use the right anchor emoji for SAS verification (#3534)
Currently, the anchor emoji has a ["Variation
Selector-16"](https://codepoints.net/U+FE0F) (U+FE0F) character after it.

The unicode specs do define U+2694 U+FE0F as a valid sequence (with suggested
rendering https://www.unicode.org/cgi-bin/varglyph?24-2693-FE0F), but our spec
spec doesn't include the variation selector, and the difference means that my
cypress tests (which attempt a verification between Element-R and unrusty
Element Web) fail intermittently.

Something of a follow-up to
https://github.com/matrix-org/matrix-js-sdk/pull/3523, but hopefully this will
be the last, because I have regenerated the whole list from the spec data.
2023-06-29 22:05:57 +00:00
Richard van der Hoff 722debe8f9 Drop support for Node 16 (#3533)
* Stop running tests on Node 16

* update package.json
2023-06-29 16:36:09 +00:00
Richard van der Hoff 5165899e82 Element-R: support for starting a SAS verification (#3528)
* integ tests: factor out some utility methods

* Add `VerificationRequest.startVerification` to replace `beginKeyVerification`

The rust SDK ties together creating the verifier and sending the
`m.key.verification.start` message, so we need to combine
`.beginKeyVerification` and `.verify`.

* add some unit tests
2023-06-29 16:34:49 +00:00
Richard van der Hoff 1828826661 QRCode: fix breakage on node 16 (#3527)
[`crypto.getRandomValues`](https://nodejs.org/docs/latest-v18.x/api/crypto.html#cryptogetrandomvaluestypedarray)
was added to the nodejs library in node 17. However, it was actually available
in node 16, hiding under
[`crypto.webcrypto`](https://nodejs.org/docs/latest-v16.x/api/webcrypto.html#cryptogetrandomvaluestypedarray). We
have some shims in `src/crypto/crypto.ts`, so let's use them.

All of this means that we don't need to monkey-patch `crypto` to run the tests
on node 16.
2023-06-29 09:58:38 +00:00
Richard van der Hoff 24cee68fa2 Rust crypto: emit VerificationRequestReceived events (#3525) 2023-06-28 14:32:27 +00:00
Richard van der Hoff e645af1fc5 use the right smiley in emoji verification (#3523) 2023-06-28 10:01:46 +00:00
Michael Telatynski de64779c27 Improve types to match reality (#3510) 2023-06-28 09:06:10 +00:00
Richard van der Hoff acbcb4658a Force coloured output from jest (#3521) 2023-06-27 15:14:47 +00:00
renovate[bot] 815484b543 Update dependency eslint-plugin-jsdoc to v46.3.0 (#3520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 12:40:26 +00:00
renovate[bot] 5a3d1a2a67 Update typescript-eslint monorepo to v5.60.0 (#3519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-06-27 12:37:30 +00:00
Richard van der Hoff 18626169e4 Create a new event type for verification requests (#3514)
* More slow test fixes

* Create a new event type for verification requests

Previous PRs (https://github.com/matrix-org/matrix-js-sdk/pull/3449, etc) have
pulled out an interface from the `VerificationRequest` class, but applications
registering for the `CryptoEvent.VerificationRequest` event could still be
expecting a fully-fledged class rather than the interface.

To handle this without breaking backwards compat, add a new event type that
carries the interface, not the class.
2023-06-27 11:24:29 +00:00
renovate[bot] e4a9f958a0 Update peter-evans/create-pull-request digest to 1534078 (#3518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 11:11:12 +00:00
Michael Telatynski ff29de743c Update README.md 2023-06-27 12:38:58 +01:00
renovate[bot] 5a68861418 Update all non-major dependencies (#3513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 11:00:37 +00:00
ElementRobot e285932776 v26.2.0-rc.1 2023-06-27 11:57:18 +01:00
ElementRobot 2af0706b16 Prepare changelog for v26.2.0-rc.1 2023-06-27 11:57:15 +01:00
Richard van der Hoff 4382d2a425 Increase another crypto test timeout (#3509)
Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3500: increase the
timeout for another test which is also timing out.
2023-06-27 10:06:48 +00:00
Kerry 9de4a057df OIDC: navigate to authorization endpoint (#3499)
* utils for authorization step in OIDC code grant

* tidy

* completeAuthorizationCodeGrant util functions

* response_mode=query

* add scope to bearertoken type

* add is_guest to whoami response type

* doc comments

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* use shimmed TextEncoder

* fetchMockJest -> fetchMock

* comment

* bearertokenresponse

* test for lowercase bearer

* handle lowercase token_type

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-06-26 23:46:53 +00:00
Richard van der Hoff b703d4a2cc More slow test fixes (#3515)
We still seem to be suffering test timeouts. Hopefully this will fix the integ tests, where #3509 etc fix the unit tests.
2023-06-26 22:15:56 +01:00
Richard van der Hoff d1dec4cd08 Implement VerificationRequest.cancel (#3505) 2023-06-26 16:56:57 +00:00
Richard van der Hoff 326a13bcfe Rearrange the verification integration tests, again (#3504)
* Element-R: Implement `CryptoApi.getVerificationRequestsToDeviceInProgress`

* Element-R: Implement `requestOwnUserVerification`

* init aliceClient *after* the fetch interceptors

* Initialise the test client separately for each test

* Avoid running all the tests twice

Currently all of these tests are running twice, with different client
configurations. That's not really adding much value; we just need to run
specific tests that way.

* Factor out functions for building responses
2023-06-26 14:44:42 +00:00
renovate[bot] e8fb47fdca Update all non-major dependencies (#3467)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 14:30:06 +00:00
Richard van der Hoff bd66e3859d Element R: Implement requestOwnUserVerification (#3508)
Part of https://github.com/vector-im/element-web/issues/25319.
2023-06-26 15:17:35 +01:00
Richard van der Hoff 96e484a3fe Element-R: implement CryptoApi.getVerificationRequestsToDeviceInProgress (#3497)
* Element-R: Implement `CryptoApi.getVerificationRequestsToDeviceInProgress`

* Element-R: Implement `requestOwnUserVerification` (#3503)

* Revert "Element-R: Implement `requestOwnUserVerification` (#3503)"

This reverts commit 8da756503c3d72b8ecbf50b4c2cf807ac36229aa.

oops, merged too soon
2023-06-26 13:31:35 +00:00
Richard van der Hoff 3e646bdfa0 Bump version of the react-sdk cypress workflow file (#3501)
`cypress.yaml` is currently pinned to an old version of the react-sdk, meaning
that each attempt to run it is currently failing with an error.

(Introduced by https://github.com/matrix-org/matrix-js-sdk/pull/3480)
2023-06-26 09:09:20 +00:00
Richard van der Hoff 48c4127035 Element-R: Basic implementation of SAS verification (#3490)
* Return uploaded keys from `/keys/query`

* Basic implementation of SAS verification in Rust

* Update the `verifier` *before* emitting `erificationRequestEvent.Change`

* remove dead code
2023-06-26 08:48:44 +00:00
Michael Telatynski f16a6bc654 Aggregate relations regardless of whether event fits into the timeline (#3496) 2023-06-26 09:39:25 +01:00
Richard van der Hoff f884c78579 Improve integration test for interactive verification (#3495)
* Tweaks to the integ test to conform to the spec

Rust is a bit more insistent than legacy crypto...

* Improve documentation on request*Verification

* Check more things in the integration test

* Create an E2EKeyResponder

* Test verification with custom method list

* Add a test for SAS cancellation

* Update spec/integ/crypto/verification.spec.ts
2023-06-23 14:38:38 +00:00
Florian Duros 3c59476cf7 Element-R: Store cross signing keys in secret storage (#3498)
* Store cross signing keys in secret storage

* Update `bootstrapSecretStorage` doc

* Throw error when `createSecretStorageKey` is not set

* Move mocking functions

* Store cross signing keys and user signing keys

* Fix `awaitCrossSigningKeyUpload` documentation

* Remove useless comment

* Fix formatting after merge conflict
2023-06-23 13:10:54 +00:00
Richard van der Hoff c8f6c4dd0d Increase crypto test timeout (#3500)
For some reason, some tests seem to be timing out in GHA. Let's try bumping up
the timeout.
2023-06-23 12:32:56 +00:00
Richard van der Hoff e8c89e9977 Element-R: speed up slow unit test (#3492)
A couple of tests were waiting for a request that wasn't happening, so timing
out after 1.5 seconds. Let's avoid the extra slowth.

(This was introduced by changes in
https://github.com/matrix-org/matrix-js-sdk/pull/3487, but the changes in this
PR do no harm anyway)
2023-06-22 09:43:39 +00:00
Kerry df78d7cf67 OIDC: add dynamic client registration util function (#3481)
* rename OidcDiscoveryError to OidcError

* oidc client registration functions

* test registerOidcClient

* tidy test file

* reexport OidcDiscoveryError for backwards compatibility
2023-06-21 21:55:25 +00:00
Michael Telatynski 80fec814a2 Add getLastUnthreadedReceiptFor utility to Thread delegating to the underlying Room (#3493) 2023-06-21 12:54:30 +01:00
Michael Telatynski 8b9672ba43 Add debug logging to figure out missing reactions in main timeline (#3494)
* Fix debug logging not working

* Add debug logging to figure out missing reactions in main timeline
2023-06-20 15:28:02 +00:00
David Baker ca00094e67 Fix bug where switching media caused media in subsequent calls to fail (#3489) 2023-06-20 15:16:43 +00:00
Richard van der Hoff 9c6d5a6c55 Element-R: wait for OlmMachine on startup (#3487)
* Element-R: wait for OlmMachine on startup

Previously, if you called `CryptoApi.getUserDeviceInfo()` before the first
`/sync` request happened, it would return an empty list, which made a bunch of
the tests racy. Add a hack to get the OlmMachine to think about its device
lists during startup.

* add a test
2023-06-20 09:29:41 +00:00
RiotRobot b77fe465f7 Resetting package fields for development 2023-06-20 10:14:37 +01:00
RiotRobot 8d93f49443 Merge branch 'master' into develop 2023-06-20 10:14:32 +01:00
RiotRobot f03fbed668 v26.1.0 2023-06-20 10:12:27 +01:00
RiotRobot c84efb57ef Prepare changelog for v26.1.0 2023-06-20 10:12:24 +01:00
Florian Duros 49f11578f7 ElementR: Add CryptoApi#bootstrapSecretStorage (#3483)
* Add WIP bootstrapSecretStorage

* Add new test if `createSecretStorageKey` is not set

* Remove old comments

* Add docs for `crypto-api.bootstrapSecretStorage`

* Remove default parameter for `createSecretStorageKey`

* Move `bootstrapSecretStorage` next to `isSecretStorageReady`

* Deprecate `bootstrapSecretStorage` in `MatrixClient`

* Update documentations

* Raise error if missing `keyInfo`

* Update behavior around `setupNewSecretStorage`

* Move `ICreateSecretStorageOpts` to `rust-crypto`

* Move `ICryptoCallbacks` to `rust-crypto`

* Update `bootstrapSecretStorage` documentation

* Add partial `CryptoCallbacks` documentation

* Fix typo

* Review changes

* Review changes
2023-06-20 08:40:11 +00:00
Richard van der Hoff 8df4be0939 Fix order of things in crypto-api.ts (#3491)
* `CryptoApi` should be first
* `export *` should be last
* everything else in the middle
2023-06-20 08:12:33 +00:00
Richard van der Hoff 80cdbe1058 Element-R: implement userHasCrossSigningKeys (#3488) 2023-06-19 21:11:04 +00:00
Michael Telatynski 9c62d15447 Specify git tags for cypress workflow so updating tests is gated by renovate PRs (#3480) 2023-06-19 07:38:24 +00:00
renovate[bot] afc70528cc Update definitelyTyped (#3465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 02:35:02 +00:00
Kerry f938d10f7b remove polls from room state on redaction (#3475) 2023-06-15 22:12:03 +00:00
Richard van der Hoff 22f0b781ea Add new methods for verification to CryptoApi (#3474)
* Add accessors for verification requests to CryptoApi

Part of https://github.com/vector-im/crypto-internal/issues/97

* Add new methods for verification to `CryptoApi` and deprecate old method

https://github.com/vector-im/crypto-internal/issues/98
2023-06-15 14:56:50 +01:00
Florian Duros 1bae10c4b2 Fix export type GeneratedSecretStorageKey (#3479) 2023-06-15 12:46:42 +00:00
Michael Telatynski 9b5b533c6f Close IDB database before deleting it to prevent spurious unexpected close errors (#3478) 2023-06-15 12:25:29 +00:00
Richard van der Hoff c425945353 Avoid deprecated classes in verification integ test (#3473)
https://github.com/matrix-org/matrix-js-sdk/pull/3449 deprecated a bunch of
exports from `src/crypto/verification/request/VerificationRequest`. Let's stop
using them in the integration test.
2023-06-15 11:24:01 +00:00
Florian Duros 0545f6df09 ElementR: Add rust-crypto#createRecoveryKeyFromPassphrase implementation (#3472)
* Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation

* Use `crypto`

* Rename `IRecoveryKey` into `GeneratedSecretStorageKey` for rust crypto

* Improve comments

* Improve `createRecoveryKeyFromPassphrase`
2023-06-14 14:38:43 +00:00
renovate[bot] d14fc426e6 Update dependency eslint-plugin-jsdoc to v46 (#3468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-14 10:49:28 +00:00
Michael Cousins 9f30defcd1 Upgrade JS-DevTools/npm-publish to v2 (#3456)
- Upgrade JS-DevTools/npm-publish to v2.2.0
- Remove workaround for bug JS-DevTools/npm-publish#15
- Remove usage of `jq` in favor of npm-publish output

Signed-off-by: Michael Cousins <michael@cousins.io>
2023-06-14 08:02:48 +00:00
renovate[bot] 9b175a4985 Update typescript-eslint monorepo to v5.59.9 (#3466)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-13 20:52:40 +00:00
Richard van der Hoff 826ea5bc58 Pull out a new VerificationRequest interface (#3449)
* add a test for incoming verification requests

* Move `VerificationRequestEvent` to crypto-api

* Move `VerificationPhase` to `crypto-api`

* Define `VerificationRequest` interface

* Implement `canAcceptVerificationRequest`
2023-06-13 11:55:37 +00:00
RiotRobot 7b316613f6 v26.1.0-rc1 2023-06-13 12:15:32 +01:00
RiotRobot 387b3485ae Prepare changelog for v26.1.0-rc1 2023-06-13 12:15:29 +01:00
Valere 9f6073478f Element-R: support for manual import/export of Room keys (#3364)
* Rust manual import/export for keys

* code review

* code review

* post merge fix

* code review

* doc: comma splice

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Better test name

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* quick doc update

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-06-13 10:28:35 +00:00
Florian Duros b47c87f909 Add rust-crypto#isCrossSigningReady implementation (#3462) 2023-06-12 14:00:31 +00:00
Kerry c66850e897 OIDC: Validate m.authentication configuration (#3419)
* validate m.authentication, fetch issuer wellknown

* move validation functions into separate file

* test validateWellKnownAuthentication

* test validateOIDCIssuerWellKnown

* add authentication cases to autodiscovery tests

* test invalid authentication config on wk

* improve types

* test case for account:false

* use hasOwnProperty in validateWellKnownAuthentication

* comments

* make registration_endpoint optional
2023-06-11 21:32:44 +00:00
Florian Duros 2766146c49 ElementR: Add CryptoApi.getCrossSigningStatus (#3452)
* Add `crypto.getCrossSigningStatus`

* Fix imports and boolean casting

* Moved `isStoredInSecretStorage` into a single function

* Review changes `CrossSigningStatus`

* Review changes for `cross-signing.spec.ts`

* Add test in case when cross signing is not setup

* Handle when the `crossSigningStatus` returned by the olmMachine is null

* Review changes for `crypto-api` documentation

* Update `cross-signing.spec.ts` according to review changes

* Moved and renamed `isStoredInSecretStorage`

* Remove noise in `CrossSigning.ts` imports

* Fix `returns` sentence in `secretStorageContainsCrossSigningKeys`

* Fix typos

* Add test for `secret-storage.ts`

* Improve documentation

* Add doc about fetch mock request name
2023-06-09 13:15:01 +00:00
Richard van der Hoff 61497c9a8f Pass Kiwi secrets to react-sdk Cypress build (#3461) 2023-06-09 11:03:33 +00:00
Richard van der Hoff 5981feeb44 Element-R: Pull out an interface from VerificationBase (#3414)
* pull out `Verifier` interface

* Mark old classes as deprecated

* Update integration tests to use new interface
2023-06-09 10:09:31 +00:00
Michael Telatynski 51218ddc1d Fix thread list being ordered based on all updates (#3458)
* Add test for thread list stability around non-reply updates

* Fix thread list being ordered based on all updates

* Fix test

* Update spec/integ/matrix-client-event-timeline.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Iterate

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-06-09 08:29:21 +00:00
RiotRobot b907433d38 Merge branch 'master' into develop 2023-06-09 09:27:19 +01:00
RiotRobot 32c0b81332 v26.0.1 2023-06-09 09:25:30 +01:00
RiotRobot 634b8ebbb4 Prepare changelog for v26.0.1 2023-06-09 09:25:28 +01:00
Andy Balaam 4ab8066e1f Merge pull request #3460 from matrix-org/backport-3455-to-staging
[Backport staging] Fix: handle `baseUrl` with trailing slash in `fetch.getUrl`
2023-06-09 09:19:58 +01:00
Kerry 13dccb3d71 Fix: handle baseUrl with trailing slash in fetch.getUrl (#3455)
* tests

* tidy trailing slash in fetch.getUrl before forming url

* make sonar happy about Polynomial regular expression used on uncontrolled data

(cherry picked from commit ef1f5bf232)
2023-06-09 08:11:56 +00:00
Kerry ef1f5bf232 Fix: handle baseUrl with trailing slash in fetch.getUrl (#3455)
* tests

* tidy trailing slash in fetch.getUrl before forming url

* make sonar happy about Polynomial regular expression used on uncontrolled data
2023-06-08 21:36:34 +00:00
Florian Duros a03e3dd501 Update @matrix-org/matrix-sdk-crypto-js to 0.1.0-alpha.10 (#3453) 2023-06-07 14:10:38 +00:00
Enrico Schwendig 3cfad3cdeb Expand webrtc stats with connection and call feed track information (#3421)
* Refactor names in webrtc stats

* Refactor summary stats reporter to gatherer

* Add call and opponent member id to call stats reports

* Update opponent member when we know them

* Add missing return type

* remove async in test

* add call feed webrtc report

* add logger for error case in stats gathering

* gather connection track report

* expand call feed stats with call feed

* formation code and fix lint issues

* clean up new track stats

 * set label for call feed stats and
 * remove stream in track stats
 * transceiver stats based on mid
 * call feed stats based on stream id
 * fix lint and test issues

* Fix merge issues

* Add test for expanding call feed stats in group call

* Fix export issue from prv PR

* explain test data and fixed some linter issues

* convert tests to snapshot tests
2023-06-07 14:05:51 +00:00
Timo 60c715d5df Extend stats summary with call device and user count based on room state (#3424)
* send expected peer connections to posthog.
(based on roomState event)

* add tests

* change GroupCallStats initialized

* prettier

* more test and catch for promise

* seperate the participant logic in a summary extend function

Signed-off-by: Timo K <toger5@hotmail.de>

* remove unused

Signed-off-by: Timo K <toger5@hotmail.de>

* rename summaryStatsReportGatherer to "Reporter"
for the summary stats there is only one instance because there is only
one summary. Since we dont have a list of gatherers it this class only reports.
Hence we rename it to be a reporter.

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* Update src/webrtc/stats/groupCallStats.ts

Co-authored-by: Robin <robin@robin.town>

* revert rename

Signed-off-by: Timo K <toger5@hotmail.de>

* Update all non-major dependencies (#3433)

* Update all non-major dependencies

* Remove name wrap-ansi-cjs

* Remove name string-width-cjs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Update definitelyTyped (#3430)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Export FALLBACK_ICE_SERVER (#3429)

* Add an integration test for verification (#3436)

* Move existing crypto integ tests into a subdirectory

* Factor out some common bits from `crypto.spec.ts`

* Integration test for device verification

* Ignore generated file in prettier

* Always show a summary after Jest tests (#3440)

... because it is otherwise impossible to see what failed.

* Use correct /v3 prefix for /refresh (#3016)

* Add tests to ensure /v3/refresh is called + automatic /v1 retry

* Request /refresh with v3 prefix, and quietly fall back to v1

* Add tests checking re-raising errors

* Update spec/unit/login.spec.ts

* Update comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update Mutual Rooms (MSC2666) support (#3381)

* update mutual rooms support

* clarify docs and switch eslint comment with todo

* please the holy linter

* change query variable names around

* add mock tests and fix issue

* ye holy linter

* GHA: build and cypress-test a copy of element-web after each push (#3412)

* Build a copy of element-web after each push

* Run cypress after each build of element-web

* Fix downstream-artifacts build (#3443)

* Fix downstream-artifacts build

* Update cypress.yml

* Fix edge cases around 2nd order relations and threads (#3437)

* Fix tests oversimplifying threads fixtures

* Check for unsigned thread_id in MatrixEvent::threadRootId

* Fix threads order being racy

* Make Sonar happier

* Iterate

* Make sliding sync linearize processing of sync requests (#3442)

* Make sliding sync linearize processing of sync requests

* Iterate

* Iterate

* Iterate

* Iterate

* Disable downstream artifacts build for develop branch (#3444)

* Export thread-related types from SDK (#3447)

* Export thread-related types from SDK

* address PR feedback

* Integration test for QR code verification (#3439)

* Integration test for QR code verification

Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3436: another
integration test, this time using the QR code flow

* Use Object.defineProperty, and restore afterwards

Apparently global.crypto exists in some environments

* apply ts-ignore

* remove stray comment

* Update spec/integ/crypto/verification.spec.ts

* Add `getShowSasCallbacks`, `getShowQrCodeCallbacks` to VerifierBase (#3422)

* Add `getShowSasCallbacks`, `getShowQrCodeCallbacks` to VerifierBase

... to avoid some type-casting

* Integration test for QR code verification

Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3436: another
integration test, this time using the QR code flow

* Rename method

... it turns out not to be used quite as I thought.

* tests for new methods

* Use Object.defineProperty, and restore afterwards

Apparently global.crypto exists in some environments

* apply ts-ignore

* More test coverage

* fix bad merge

* Fix changelog_head.py script to be Python 3 compatible

* Prepare changelog for v25.2.0-rc.1

* v25.2.0-rc.1

* Fix tsconfig-build.json

* Prepare changelog for v25.2.0-rc.2

* v25.2.0-rc.2

* Fix docs deployment

* Prepare changelog for v25.2.0-rc.3

* v25.2.0-rc.3

* Prepare changelog for v25.2.0-rc.4

* v25.2.0-rc.4

* [Backport staging] Attempt a potential workaround for stuck notifs (#3387)

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>

* Prepare changelog for v25.2.0-rc.5

* v25.2.0-rc.5

* [Backport staging] Fix mark as unread button (#3401)

Co-authored-by: Michael Weimann <michaelw@matrix.org>

* Prepare changelog for v26.0.0-rc.1

* v26.0.0-rc.1

* Prepare changelog for v26.0.0

* v26.0.0

* Resetting package fields for development

* use cli.canSupport to determine intentional mentions support (#3445)

* use cli.canSupport to determine intentional mentions support

* more specific comment

* Update src/client.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* git fixup

Signed-off-by: Timo K <toger5@hotmail.de>

* import updates

Signed-off-by: Timo K <toger5@hotmail.de>

* dont revert enricos change

Signed-off-by: Timo K <toger5@hotmail.de>

* temp rename for lowercase

* lowercase

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Robin <robin@robin.town>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: David Lee <david@david-lee.net>
Co-authored-by: Jonathan de Jong <jonathan@automatia.nl>
Co-authored-by: Stanislav Demydiuk <stas-demydiuk@users.noreply.github.com>
Co-authored-by: ElementRobot <releases@riot.im>
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
Co-authored-by: Michael Weimann <michaelw@matrix.org>
Co-authored-by: Kerry <kerrya@element.io>
2023-06-07 13:36:40 +00:00
Richard van der Hoff c2942ddbc7 Add new methods to VerificationRequest (#3441)
* Add new method `VerificationRequest.getQRCodeBytes`

... which requires fewer complicated classes than the existing `qrCodeData`

* Add new property `VerificationRequest.otherDeviceId`

... to save going via `.channel`

* Add more methods to `VerificationRequest`

... to avoid the need for `channel`

* Use new methods in integration tests
2023-06-07 13:23:27 +00:00
Michael Weimann 2d7fdde7ed Update MSC3912 implementation to use with_rel_type instead of with_relations (#3420)
* Migrate MSC3912

* Fix doc blocks

* Remove with_relations fallback

* Implement PR feedback

* Fix typo
2023-06-07 12:05:14 +00:00
Enrico Schwendig cf34e90cb4 Fix export issue from thread module (#3451)
* Fix export issue from thread module

* separate type from class export

* follow the pattern of what other exports do
2023-06-07 09:35:20 +00:00
Kerry f0fa4d2cc8 use cli.canSupport to determine intentional mentions support (#3445)
* use cli.canSupport to determine intentional mentions support

* more specific comment

* Update src/client.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-06-06 21:52:55 +00:00
RiotRobot c52e4b6329 Resetting package fields for development 2023-06-06 13:45:40 +01:00
RiotRobot 9451b55985 Merge branch 'master' into develop 2023-06-06 13:45:34 +01:00
RiotRobot f41fa84e72 v26.0.0 2023-06-06 13:44:07 +01:00
RiotRobot 3c4134537a Prepare changelog for v26.0.0 2023-06-06 13:44:03 +01:00
Richard van der Hoff 39714bfe6f Add getShowSasCallbacks, getShowQrCodeCallbacks to VerifierBase (#3422)
* Add `getShowSasCallbacks`, `getShowQrCodeCallbacks` to VerifierBase

... to avoid some type-casting

* Integration test for QR code verification

Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3436: another
integration test, this time using the QR code flow

* Rename method

... it turns out not to be used quite as I thought.

* tests for new methods

* Use Object.defineProperty, and restore afterwards

Apparently global.crypto exists in some environments

* apply ts-ignore

* More test coverage

* fix bad merge
2023-06-06 11:16:19 +00:00
Richard van der Hoff f5f6100b1e Integration test for QR code verification (#3439)
* Integration test for QR code verification

Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3436: another
integration test, this time using the QR code flow

* Use Object.defineProperty, and restore afterwards

Apparently global.crypto exists in some environments

* apply ts-ignore

* remove stray comment

* Update spec/integ/crypto/verification.spec.ts
2023-06-06 10:22:12 +00:00
Stanislav Demydiuk ecd700a36e Export thread-related types from SDK (#3447)
* Export thread-related types from SDK

* address PR feedback
2023-06-06 07:59:10 +00:00
Richard van der Hoff ea7042efb9 Disable downstream artifacts build for develop branch (#3444) 2023-06-05 17:38:08 +00:00
Michael Telatynski 04a6c4e6c4 Make sliding sync linearize processing of sync requests (#3442)
* Make sliding sync linearize processing of sync requests

* Iterate

* Iterate

* Iterate

* Iterate
2023-06-05 16:40:19 +00:00
Michael Telatynski 0329824cab Fix edge cases around 2nd order relations and threads (#3437)
* Fix tests oversimplifying threads fixtures

* Check for unsigned thread_id in MatrixEvent::threadRootId

* Fix threads order being racy

* Make Sonar happier

* Iterate
2023-06-05 15:18:56 +00:00
Richard van der Hoff 3351c4f57a Fix downstream-artifacts build (#3443)
* Fix downstream-artifacts build

* Update cypress.yml
2023-06-05 15:05:14 +00:00
Richard van der Hoff e70a1a1eff GHA: build and cypress-test a copy of element-web after each push (#3412)
* Build a copy of element-web after each push

* Run cypress after each build of element-web
2023-06-05 09:11:00 +00:00
Jonathan de Jong 1a5af9d8e3 Update Mutual Rooms (MSC2666) support (#3381)
* update mutual rooms support

* clarify docs and switch eslint comment with todo

* please the holy linter

* change query variable names around

* add mock tests and fix issue

* ye holy linter
2023-06-05 08:23:44 +00:00
David Lee 258f157ebc Use correct /v3 prefix for /refresh (#3016)
* Add tests to ensure /v3/refresh is called + automatic /v1 retry

* Request /refresh with v3 prefix, and quietly fall back to v1

* Add tests checking re-raising errors

* Update spec/unit/login.spec.ts

* Update comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-06-03 14:56:12 +00:00
Richard van der Hoff dfb079a76f Always show a summary after Jest tests (#3440)
... because it is otherwise impossible to see what failed.
2023-06-02 17:38:17 +00:00
Richard van der Hoff 858155e0ef Add an integration test for verification (#3436)
* Move existing crypto integ tests into a subdirectory

* Factor out some common bits from `crypto.spec.ts`

* Integration test for device verification

* Ignore generated file in prettier
2023-06-02 15:01:21 +00:00
Michael Telatynski 946a1cef0f Export FALLBACK_ICE_SERVER (#3429) 2023-06-02 14:29:49 +00:00
renovate[bot] 6c1fdbb7e9 Update definitelyTyped (#3430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-06-02 13:36:53 +00:00
renovate[bot] 8cd7c96496 Update all non-major dependencies (#3433)
* Update all non-major dependencies

* Remove name wrap-ansi-cjs

* Remove name string-width-cjs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-06-02 13:33:05 +00:00
renovate[bot] cc9d530bcf Update dependency eslint-plugin-jsdoc to v45 (#3435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-01 16:38:45 +00:00
renovate[bot] cabf6da6a7 Update typescript-eslint monorepo to v5.59.7 (#3432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-01 16:38:30 +00:00
renovate[bot] 895a82efcd Update dependency @types/jest to v29.5.2 (#3431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-01 16:38:26 +00:00
ElementRobot d4414341d6 v26.0.0-rc.1 2023-06-01 16:54:25 +01:00
ElementRobot f173014fc0 Prepare changelog for v26.0.0-rc.1 2023-06-01 16:54:22 +01:00
ElementRobot 81cb44db7f Merge branch 'develop' into staging
# Conflicts:
#	spec/unit/room.spec.ts
#	src/models/event-timeline.ts
#	src/models/thread.ts
2023-06-01 16:49:33 +01:00
Michael Telatynski 71f9b25db7 Ensure we do not add relations to the wrong timeline (#3427)
* Do not assume that a relation lives in main timeline if we do not know its parent

* For pagination, partition relations with unknown parents into a separate bucket

And only add them to relation map, no timelines

* Make addLiveEvents async and have it fetch parent events of unknown relations to not insert into the wrong timeline

* Fix tests not awaiting addLIveEvents

* Fix handling of thread roots in eventShouldLiveIn

* Fix types

* Fix tests

* Fix import

* Stash thread ID of relations in unsigned to be stashed in sync accumulator

* Persist after processing

* Revert "Persist after processing"

This reverts commit 05ed6409b35f5e9bea3b699d0abcaac3d02588c5.

* Update unsigned field name to match MSC4023

* Persist after processing to store thread id in unsigned sync accumulator

* Add test

* Fix replayEvents getting doubled up due to Thread::addEvents being called in createThread and separately

* Fix test

* Switch to using UnstableValue

* Add comment

* Iterate
2023-06-01 15:29:05 +00:00
Enrico Schwendig 9c5c7ddb17 Add remote user and call id in webrtc call stats (#3413)
* Refactor names in webrtc stats

* Refactor summary stats reporter to gatherer

* Add call and opponent member id to call stats reports

* Update opponent member when we know them

* Add missing return type

* remove async in test

* mark new stats property as optional to avoid braking changes
2023-06-01 15:07:44 +00:00
sigmaSd feb424b0a9 mention deno support in the README (#3417)
* mention deno support in the README

It seems to work fine, and I have a demo bot with it https://github.com/sigmaSd/deno-matrix-bot

* Update README.md
2023-06-01 07:52:55 +00:00
Travis Ralston 648a1a09b1 Mark room version 10 as safe (#3425)
As should have been done a year ago.
2023-05-31 14:15:49 +00:00
Michael Telatynski 56c5375bbd Remove spec non-compliant extended glob format (#3423)
* Remove spec non-compliant extended glob format

* Simplify

* Remove tests for non spec compliant behaviour

* Remove stale rules
2023-05-31 09:05:55 +00:00
Andy Balaam b29e1e9d21 Adjust fetchEditsWhereNeeded to use a clearer filter and async function (#3411)
* Make a clear and explicit filter on which events are considered for fetchEventsWhereNeeded

* Convert the logic in fetchEventsWhereNeeded to an async function
2023-05-26 08:29:35 +00:00
Enrico Schwendig 7ade461a4c Refactor naming of webrtc stats reporter (#3404)
* Refactor names in webrtc stats

* Refactor summary stats reporter to gatherer

* Add test for signal change

* rename test
2023-05-26 07:56:49 +00:00
Andy Balaam b5414ea914 Fix bug where original event was inserted into timeline instead of the edit event (#3398)
* Fix an existing test for editing messages in threads

While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.

* Move editing test into thread.spec.ts

* Isolate Thread global modification in beforeAll()

* Delete unneeded setUnsigned call

* Use standard message-creation methods

* Rename event variables

* Rename sender->user

* Remove unneeded variables

* Extract distractions into functions

* Fetch edits for thread messages

Modifies fetchEditsWhereNeeded to allow edits of threaded messages. The
code before prevented any relations from fetching edits, but of course
events in threads are relations.

We definitely want thread messages to get their edits fetched, and I
assume this is working in the real code, probably because the event
being looked at is some kind of eventmapped thing that doesn't have
proper relations visible on it.

In tests, if we don't make this change, we can't see edits getting
fetched.

* Add a test for fetching edits on demand in a thread

This test demonstrates the current behaviour, which contains a bug - we
don't actually add the right event to the timeline.

* Fix bug where original event was inserted into timeline instead of the edit event
2023-05-25 15:48:48 +00:00
Andy Balaam 711bf4710d Test inserting edit events into the thread timeline "on demand" (#3410)
* Fix an existing test for editing messages in threads

While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.

* Move editing test into thread.spec.ts

* Isolate Thread global modification in beforeAll()

* Delete unneeded setUnsigned call

* Use standard message-creation methods

* Rename event variables

* Rename sender->user

* Remove unneeded variables

* Extract distractions into functions

* Fetch edits for thread messages

Modifies fetchEditsWhereNeeded to allow edits of threaded messages. The
code before prevented any relations from fetching edits, but of course
events in threads are relations.

We definitely want thread messages to get their edits fetched, and I
assume this is working in the real code, probably because the event
being looked at is some kind of eventmapped thing that doesn't have
proper relations visible on it.

In tests, if we don't make this change, we can't see edits getting
fetched.

* Add a test for fetching edits on demand in a thread

This test demonstrates the current behaviour, which contains a bug - we
don't actually add the right event to the timeline.
2023-05-25 16:32:42 +01:00
Andy Balaam 48b60bb885 Simplify thread-editing test (#3409)
* Fix an existing test for editing messages in threads

While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.

* Move editing test into thread.spec.ts

* Isolate Thread global modification in beforeAll()

* Delete unneeded setUnsigned call

* Use standard message-creation methods

* Rename event variables

* Rename sender->user

* Remove unneeded variables

* Extract distractions into functions
2023-05-25 14:03:58 +00:00
Andy Balaam 7ed14dc749 Only add a local receipt if it's after an existing receipt (#3399)
* Add a test for creating local echo receipts in threads

* Only add local receipt if it's after existing receipt

* Refactor local receipt tests to be shorter

* Tests for local receipts where we DO have recursive relations support
2023-05-25 13:59:14 +00:00
Andy Balaam 26736b6bb1 Move editing test into thread.spec.ts (#3408)
* Fix an existing test for editing messages in threads

While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.

* Move editing test into thread.spec.ts
2023-05-25 13:20:59 +00:00
Andy Balaam 056aae823d Fix an existing test for editing messages in threads (#3407)
While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.
2023-05-25 12:38:53 +00:00
Andy Balaam d1bfdca0c9 Isolate changes to Thread global into a single describe block (#3390) 2023-05-25 11:45:47 +00:00
Andy Balaam c0577c29c4 Minor improvements to the code inserting events into timelines (#3389)
* Remove unneeded pling

* Comment clarifying why we bail out in insertEventIntoTimeline
2023-05-25 11:44:56 +00:00
Richard van der Hoff a2d1dee0a1 Minor fixes to CryptoApi doc comments (#3406)
Followups to #3287 and #3360: minor clarifications to the doc-comments
2023-05-24 16:02:40 +00:00
Richard van der Hoff d7bcdff29b Fix re-exports for SAS.ts (#3405)
These three are only types, not objects we can export.

Fixes warnings in EW (and probably some build failures for someone somewhere):

```
2023-05-24 11:27:28.294 [element-js] WARNING in ../matrix-js-sdk/src/crypto/verification/SAS.ts 31:0-123
2023-05-24 11:27:28.294 [element-js] "export 'EmojiMapping' was not found in '../../crypto-api/verification'
2023-05-24 11:27:28.294 [element-js]
2023-05-24 11:27:28.294 [element-js] WARNING in ../matrix-js-sdk/src/crypto/verification/SAS.ts 31:0-123
2023-05-24 11:27:28.294 [element-js] "export 'GeneratedSas' (reexported as 'IGeneratedSas') was not found in '../../crypto-api/verification'
2023-05-24 11:27:28.294 [element-js]
2023-05-24 11:27:28.294 [element-js] WARNING in ../matrix-js-sdk/src/crypto/verification/SAS.ts 31:0-123
2023-05-24 11:27:28.294 [element-js] "export 'ShowSasCallbacks' (reexported as 'ISasEvent') was not found in '../../crypto-api/verification'
```
2023-05-24 10:54:10 +00:00
Eric Eastwood ed71cdeccd Add more context for why threaded vs unthreaded read receipts (#3378)
* Add more context for why threaded vs unthreaded read receipts

See https://github.com/matrix-org/matrix-js-sdk/pull/3339#discussion_r1195364120

* Language updates from Andy

See https://github.com/matrix-org/matrix-js-sdk/pull/3378#discussion_r1197622113

* Fix lints
2023-05-24 10:48:22 +00:00
Michael Telatynski 729f924de1 Prioritise entirely supported flows for UIA (#3402)
* Prioritise entirely supported flows for UIA

* Add tests

* Fix types

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/interactive-auth.ts

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-05-24 10:33:48 +00:00
Andy Balaam 4732098731 Test for inserting events into the timeline in timestamp order (#3391)
* Move mkReaction into test-utils so it can be used by other code

* Basic test for inserting messages into the thread timeline
2023-05-24 10:29:57 +00:00
ElementRobot a7d503a8ac [Backport staging] Fix mark as unread button (#3401)
Co-authored-by: Michael Weimann <michaelw@matrix.org>
2023-05-24 09:02:18 +01:00
Michael Weimann 063d69eff1 Fix mark as unread button (#3393)
* Fix mark as unread button

* Revert to prefer the last event from the main timeline

* Refactor room test

* Fix type

* Improve docs

* Insert events to the end of the timeline

* Improve test doc
2023-05-23 20:34:18 +00:00
Richard van der Hoff 614f446361 Combine QrCodeEvent, SasEvent and VerificationEvent (#3386)
* Move IReciprocateQr to `crypto-api` and rename

* Move ISasEvent to `crypto-api`, and rename

... and add some comments

* Combine QrCodeEvent, SasEvent and VerificationEvent together

... as a precursor to extracting a single `Verifier` interface for `SAS` and `ReciprocateQRCode`.

`enum`s are slightly magical things that have both a type and a value, so we
have to re-export their backwards-compatibility fudges twice.

* Update src/crypto/verification/Base.ts
2023-05-22 11:02:10 +00:00
Michael Telatynski b62fac9dad Update client.ts 2023-05-19 16:39:02 +01:00
ElementRobot ff07cb642f v25.2.0-rc.5 2023-05-19 16:35:34 +01:00
ElementRobot fba3cf2756 Prepare changelog for v25.2.0-rc.5 2023-05-19 16:35:32 +01:00
Michael Telatynski 5973c66726 Make sonar happier about our code & tests (#3388) 2023-05-19 16:33:19 +01:00
ElementRobot b22fc1f9d9 [Backport staging] Attempt a potential workaround for stuck notifs (#3387)
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2023-05-19 15:57:55 +01:00
Richard van der Hoff 3f48a954d8 Move crypto classes into a separate namespace (#3385)
* Move crypto classes into a separate namespace

* Add in re-exports for backwards compatibility

* Update src/matrix.ts
2023-05-19 12:43:16 +00:00
Michael Telatynski b5d544df68 Update sonarqube.yml (#3376) 2023-05-19 12:16:29 +00:00
Andy Balaam cd26ba67d4 Attempt a potential workaround for stuck notifs (#3384)
* Attempt a potential workaround for stuck notifs

* Remove TODOs

* Fix backwards logic about server support for MSC3981 in fetchEditsWhereNeeded

* Check for lack of MSC3981 server support before calling insertEventIntoTimeline

* If no parent event is found, insert purely based on timestamp

* Mark temporary methods as internal
2023-05-19 12:34:26 +01:00
Michael Telatynski 488390365a Add typedoc-plugin-coverage (#3379)
* Add typedoc-plugin-coverage

* Specify coverageLabel
2023-05-19 10:35:19 +00:00
Richard van der Hoff b7b1129478 Fix bug with pendingEventOrdering: "chronological" (#3382)
If we don't do this, then we end up trying to match the MAC on our own
`m.key.verification.mac`, which of course fails.
2023-05-18 13:47:24 +00:00
Richard van der Hoff 0fa9528a3f Comments for typed-event-emitter classes (#3380)
* Comments for typed-event-emitter classes

* Export `TypedEventEmitter`

... to stop tsdoc complaining.
2023-05-18 12:22:51 +00:00
Enrico Schwendig a7b1dcaf95 Filter all summary stats which collect the first time webrtc metrics. (#3377)
* remove comment

* Filter all summery stats which collect the first time webrtc stats

* Fix lint issues

* Fix lint second issues
2023-05-17 15:40:33 +00:00
Richard van der Hoff bb5bccbf78 Element-R: initial implementation of bootstrapCrossSigning (#3368)
* Working `bootstrapCrossSigning` for rust

* Remove unused `oldBackendOnly`

* update tests

* another test
2023-05-16 20:30:32 +00:00
renovate[bot] ece3ccb958 Lock file maintenance (#3375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 16:31:36 +00:00
renovate[bot] a769cf88f7 Update dependency eslint-plugin-unicorn to v47 (#3373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 15:23:34 +00:00
renovate[bot] 8b0f1a0c59 Update dependency @types/node to v18.16.9 (#3370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-05-16 15:19:43 +00:00
renovate[bot] 978e748246 Update dependency eslint-plugin-jsdoc to v44 (#3372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 15:15:21 +00:00
renovate[bot] 32a5cc4728 Update matrix-org/netlify-pr-preview action to v2 (#3374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 15:01:13 +00:00
renovate[bot] d580f56c0b Update all non-major dependencies (#3371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 14:51:21 +00:00
Michael Telatynski 789458e0d4 Add methods to terminate idb worker (#3362) 2023-05-16 16:05:55 +01:00
renovate[bot] 8d14d45272 Update typescript-eslint monorepo to v5.59.6 (#3334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 14:37:05 +00:00
214 changed files with 23013 additions and 5623 deletions
+1 -4
View File
@@ -66,15 +66,12 @@ module.exports = {
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"jest/no-disabled-tests": "off",
// TODO: There are many tests with invalid expects that should be fixed,
// https://github.com/matrix-org/matrix-js-sdk/issues/2976
"jest/valid-expect": "off",
// Also treat "oldBackendOnly" as a test function.
// Used in some crypto tests.
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly", "newBackendOnly"],
},
],
},
+2
View File
@@ -3,4 +3,6 @@
/package.json @matrix-org/element-web-app-team
/yarn.lock @matrix-org/element-web-app-team
/src/webrtc @matrix-org/element-call-reviewers
/src/matrixrtc @matrix-org/element-call-reviewers
/spec/*/webrtc @matrix-org/element-call-reviewers
/spec/*/matrixrtc @matrix-org/element-call-reviewers
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
)
)
steps:
- uses: tibdex/backport@2e217641d82d02ba0603f46b1aeedefb258890ac # v2
- uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # 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
+31
View File
@@ -0,0 +1,31 @@
# Triggers after the "Downstream artifacts" build has finished, to run the
# cypress tests (with access to repo secrets)
name: matrix-react-sdk Cypress End to End Tests
on:
workflow_run:
workflows: ["Build downstream artifacts"]
types:
- completed
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
jobs:
cypress:
name: Cypress
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.79.0
permissions:
actions: read
issues: read
statuses: write
pull-requests: read
secrets:
# secrets are not automatically shared with called workflows, so share the cypress dashboard key, and the Kiwi login details
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
TCMS_USERNAME: ${{ secrets.TCMS_USERNAME }}
TCMS_PASSWORD: ${{ secrets.TCMS_PASSWORD }}
with:
react-sdk-repository: matrix-org/matrix-react-sdk
rust-crypto: true
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
path: docs
- name: 📤 Deploy to Netlify
uses: matrix-org/netlify-pr-preview@v1
uses: matrix-org/netlify-pr-preview@v2
with:
path: docs
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
@@ -0,0 +1,27 @@
name: Build downstream artifacts
on:
# We only want the Rust Crypto Cypress tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume 4 concurrent runners.
# Anyone working on Rust Crypto is able to run the tests locally if required.
merge_group:
types: [checks_requested]
# For now at least, we don't run this or the cypress-tests against pushes
# to develop or master.
#
# Note that if we later choose to do so, we'll need to find a way to stop
# the results in Cypress Cloud from clobbering those from the 'develop'
# branch of matrix-react-sdk.
#
#push:
# branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-element-web:
name: Build element-web
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.79.0
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
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@26b39ed245ab8f31526069329e112ab2fb224588 # v2
uses: peter-evans/repository-dispatch@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
+7 -9
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
@@ -24,18 +24,16 @@ jobs:
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
uses: JS-DevTools/npm-publish@5a85faf05d2ade2d5b6682bfe5359915d5159c6c # v2.2.1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
tag: next
ignore-scripts: false
- 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
if: github.event.release.prerelease == false && steps.npm-publish.outputs.id
run: npm dist-tag add "$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 }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.id }}
+2 -2
View File
@@ -9,10 +9,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: gh-pages
path: _docs
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
prepare:
name: Prepare
if: github.event.workflow_run.event != 'merge_group'
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
runs-on: ubuntu-latest
outputs:
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
@@ -36,7 +36,7 @@ jobs:
sonarqube:
name: 🩻 SonarQube
if: github.event.workflow_run.event != 'merge_group'
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
needs: prepare
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
+3 -3
View File
@@ -13,7 +13,7 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
@@ -39,7 +39,7 @@ jobs:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
@@ -55,7 +55,7 @@ jobs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
+5 -2
View File
@@ -18,10 +18,10 @@ jobs:
strategy:
matrix:
specs: [browserify, integ, unit]
node: [16, 18, latest]
node: [18, latest]
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v3
@@ -50,6 +50,9 @@ jobs:
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
# tell jest to use coloured output
FORCE_COLOR: true
- name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true'
run: mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
+2 -2
View File
@@ -9,7 +9,7 @@ jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
@@ -20,7 +20,7 @@ jobs:
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 # v5
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/upgrade-deps
+3
View File
@@ -24,3 +24,6 @@ out
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
/CHANGELOG.md
# This file is also autogenerated
/spec/test-utils/test-data/index.ts
+196 -2
View File
@@ -1,10 +1,199 @@
Changes in [25.2.0-rc.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.2.0-rc.4) (2023-05-16)
Changes in [28.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.2.0) (2023-09-26)
==================================================================================================
## 🦖 Deprecations
* Implement `getEncryptionInfoForEvent` and deprecate `getEventEncryptionInfo` ([\#3693](https://github.com/matrix-org/matrix-js-sdk/pull/3693)).
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* Delete knocked room when knock membership changes ([\#3729](https://github.com/matrix-org/matrix-js-sdk/pull/3729)). Contributed by @maheichyk.
* Introduce MatrixRTCSession lower level group call primitive ([\#3663](https://github.com/matrix-org/matrix-js-sdk/pull/3663)).
* Sync knock rooms ([\#3703](https://github.com/matrix-org/matrix-js-sdk/pull/3703)). Contributed by @maheichyk.
## 🐛 Bug Fixes
* Dont access indexed db when undefined ([\#3707](https://github.com/matrix-org/matrix-js-sdk/pull/3707)). Contributed by @finsterwalder.
* Don't reset unread count when adding a synthetic receipt ([\#3706](https://github.com/matrix-org/matrix-js-sdk/pull/3706)). Fixes #3684. Contributed by @andybalaam.
Changes in [28.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.1.0) (2023-09-12)
============================================================================================================
## 🦖 Deprecations
* Deprecate device methods in MatrixClient ([\#3357](https://github.com/matrix-org/matrix-js-sdk/pull/3357)).
* Deprecate `MatrixClient.checkUserTrust` ([\#3691](https://github.com/matrix-org/matrix-js-sdk/pull/3691)).
* Deprecate `MatrixClient.{prepare,create}KeyBackupVersion` in favour of new `CryptoApi.resetKeyBackup` API ([\#3689](https://github.com/matrix-org/matrix-js-sdk/pull/3689)).
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* Allow calls without ICE/TURN/STUN servers ([\#3695](https://github.com/matrix-org/matrix-js-sdk/pull/3695)).
* Emit summary update event ([\#3687](https://github.com/matrix-org/matrix-js-sdk/pull/3687)). Fixes vector-im/element-web#26033.
* ElementR: Update `CryptoApi.userHasCrossSigningKeys` ([\#3646](https://github.com/matrix-org/matrix-js-sdk/pull/3646)). Contributed by @florianduros.
* Add `join_rule` field to /publicRooms response ([\#3673](https://github.com/matrix-org/matrix-js-sdk/pull/3673)). Contributed by @charlynguyen.
* Use sender instead of content.creator field on m.room.create events ([\#3675](https://github.com/matrix-org/matrix-js-sdk/pull/3675)).
## 🐛 Bug Fixes
* Provide better error for ICE Server SyntaxError ([\#3694](https://github.com/matrix-org/matrix-js-sdk/pull/3694)). Fixes vector-im/element-web#21804.
* Legacy crypto: re-check key backup after `bootstrapSecretStorage` ([\#3692](https://github.com/matrix-org/matrix-js-sdk/pull/3692)). Fixes vector-im/element-web#26115.
Changes in [28.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.0.0) (2023-08-29)
==================================================================================================
## 🚨 BREAKING CHANGES
* Set minimum supported Matrix 1.1 version (drop legacy r0 versions) ([\#3007](https://github.com/matrix-org/matrix-js-sdk/pull/3007)). Fixes vector-im/element-web#16876.
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* ElementR: Add `CryptoApi.requestVerificationDM` ([\#3643](https://github.com/matrix-org/matrix-js-sdk/pull/3643)). Contributed by @florianduros.
* Implement `CryptoApi.checkKeyBackupAndEnable` ([\#3633](https://github.com/matrix-org/matrix-js-sdk/pull/3633)). Fixes vector-im/crypto-internal#111 and vector-im/crypto-internal#112.
## 🐛 Bug Fixes
* ElementR: Process all verification events, not just requests ([\#3650](https://github.com/matrix-org/matrix-js-sdk/pull/3650)). Contributed by @florianduros.
Changes in [27.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.2.0) (2023-08-15)
==================================================================================================
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* Allow knocking rooms ([\#3647](https://github.com/matrix-org/matrix-js-sdk/pull/3647)). Contributed by @charlynguyen.
* Bump pagination limit to account for threaded events ([\#3638](https://github.com/matrix-org/matrix-js-sdk/pull/3638)).
* ElementR: Add `CryptoApi.findVerificationRequestDMInProgress` ([\#3601](https://github.com/matrix-org/matrix-js-sdk/pull/3601)). Contributed by @florianduros.
* Export more into the public interface ([\#3614](https://github.com/matrix-org/matrix-js-sdk/pull/3614)).
## 🐛 Bug Fixes
* Fix wrong handling of encrypted rooms when loading them from sync accumulator ([\#3640](https://github.com/matrix-org/matrix-js-sdk/pull/3640)). Fixes vector-im/element-web#25803.
* Skip processing thread roots and fetching threads list when support is disabled ([\#3642](https://github.com/matrix-org/matrix-js-sdk/pull/3642)).
* Ensure we don't overinflate the total notification count ([\#3634](https://github.com/matrix-org/matrix-js-sdk/pull/3634)). Fixes vector-im/element-web#25803.
Changes in [27.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.1.0) (2023-08-01)
==================================================================================================
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* ElementR: Add `CryptoApi.getCrossSigningKeyId` ([\#3619](https://github.com/matrix-org/matrix-js-sdk/pull/3619)). Contributed by @florianduros.
* ElementR: Stub `CheckOwnCrossSigningTrust`, import cross signing keys and verify local device in `bootstrapCrossSigning` ([\#3608](https://github.com/matrix-org/matrix-js-sdk/pull/3608)). Contributed by @florianduros.
* Specify /preview_url requests as low priority ([\#3609](https://github.com/matrix-org/matrix-js-sdk/pull/3609)). Fixes vector-im/element-web#7292.
* Element-R: support for displaying QR codes during verification ([\#3588](https://github.com/matrix-org/matrix-js-sdk/pull/3588)). Fixes vector-im/crypto-internal#124.
* Add support for scanning QR codes during verification, with Rust crypto ([\#3565](https://github.com/matrix-org/matrix-js-sdk/pull/3565)).
* Add methods to influence set_presence on /sync API calls ([\#3578](https://github.com/matrix-org/matrix-js-sdk/pull/3578)).
## 🐛 Bug Fixes
* Fix threads ending up with chunks of their timelines missing ([\#3618](https://github.com/matrix-org/matrix-js-sdk/pull/3618)). Fixes vector-im/element-web#24466.
* Ensure we do not clobber a newer RR with an older unthreaded one ([\#3617](https://github.com/matrix-org/matrix-js-sdk/pull/3617)). Fixes vector-im/element-web#25806.
* Fix registration check your emails stage regression ([\#3616](https://github.com/matrix-org/matrix-js-sdk/pull/3616)).
* Fix how `Room::eventShouldLiveIn` handles replies to unknown parents ([\#3615](https://github.com/matrix-org/matrix-js-sdk/pull/3615)). Fixes vector-im/element-web#22603.
* Only send threaded read receipts if threads support is enabled ([\#3612](https://github.com/matrix-org/matrix-js-sdk/pull/3612)).
* ElementR: Fix `userId` parameter usage in `CryptoApi#getVerificationRequestsToDeviceInProgress` ([\#3611](https://github.com/matrix-org/matrix-js-sdk/pull/3611)). Contributed by @florianduros.
* Fix edge cases around non-thread relations to thread roots and read receipts ([\#3607](https://github.com/matrix-org/matrix-js-sdk/pull/3607)).
* Fix read receipt sending behaviour around thread roots ([\#3600](https://github.com/matrix-org/matrix-js-sdk/pull/3600)).
* Export typed event emitter key types ([\#3597](https://github.com/matrix-org/matrix-js-sdk/pull/3597)). Fixes #3506.
* Element-R: ensure that `userHasCrossSigningKeys` uses up-to-date data ([\#3599](https://github.com/matrix-org/matrix-js-sdk/pull/3599)). Fixes vector-im/element-web#25773.
* Fix sending `auth: null` due to broken types around UIA ([\#3594](https://github.com/matrix-org/matrix-js-sdk/pull/3594)).
Changes in [27.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.0.0) (2023-07-18)
==================================================================================================
## 🚨 BREAKING CHANGES
* Drop support for Node 16 ([\#3533](https://github.com/matrix-org/matrix-js-sdk/pull/3533)).
* Improve types around login, registration, UIA and identity servers ([\#3537](https://github.com/matrix-org/matrix-js-sdk/pull/3537)).
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
* Simplify `MatrixClient::setPowerLevel` API ([\#3570](https://github.com/matrix-org/matrix-js-sdk/pull/3570)). Fixes vector-im/element-web#13900 and #1844.
* Deprecate `VerificationRequest.getQRCodeBytes` and replace it with the asynchronous `generateQRCode`. ([\#3562](https://github.com/matrix-org/matrix-js-sdk/pull/3562)).
* Deprecate `VerificationRequest.beginKeyVerification()` in favour of `VerificationRequest.startVerification()`. ([\#3528](https://github.com/matrix-org/matrix-js-sdk/pull/3528)).
* Deprecate `Crypto.VerificationRequest` application event, replacing it with `Crypto.VerificationRequestReceived`. ([\#3514](https://github.com/matrix-org/matrix-js-sdk/pull/3514)).
## ✨ Features
* Throw saner error when peeking has its room pulled out from under it ([\#3577](https://github.com/matrix-org/matrix-js-sdk/pull/3577)). Fixes vector-im/element-web#18679.
* OIDC: Log in ([\#3554](https://github.com/matrix-org/matrix-js-sdk/pull/3554)). Contributed by @kerryarchibald.
* Prevent threads code from making identical simultaneous API hits ([\#3541](https://github.com/matrix-org/matrix-js-sdk/pull/3541)). Fixes vector-im/element-web#25395.
* Update IUnsigned type to be extensible ([\#3547](https://github.com/matrix-org/matrix-js-sdk/pull/3547)).
* add stop() api to BackupManager for clean shutdown ([\#3553](https://github.com/matrix-org/matrix-js-sdk/pull/3553)).
* Log the message ID of any undecryptable to-device messages ([\#3543](https://github.com/matrix-org/matrix-js-sdk/pull/3543)).
* Ignore thread relations on state events for consistency with edits ([\#3540](https://github.com/matrix-org/matrix-js-sdk/pull/3540)).
* OIDC: validate id token ([\#3531](https://github.com/matrix-org/matrix-js-sdk/pull/3531)). Contributed by @kerryarchibald.
## 🐛 Bug Fixes
* Fix read receipt sending behaviour around thread roots ([\#3600](https://github.com/matrix-org/matrix-js-sdk/pull/3600)).
* Fix `TypedEventEmitter::removeAllListeners(void)` not working ([\#3561](https://github.com/matrix-org/matrix-js-sdk/pull/3561)).
* Don't allow Olm unwedging rate-limiting to race ([\#3549](https://github.com/matrix-org/matrix-js-sdk/pull/3549)). Fixes vector-im/element-web#25716.
* Fix an instance of failed to decrypt error when an in flight `/keys/query` fails. ([\#3486](https://github.com/matrix-org/matrix-js-sdk/pull/3486)).
* Use the right anchor emoji for SAS verification ([\#3534](https://github.com/matrix-org/matrix-js-sdk/pull/3534)).
* fix a bug which caused the wrong emoji to be shown during SAS device verification. ([\#3523](https://github.com/matrix-org/matrix-js-sdk/pull/3523)).
Changes in [26.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.2.0) (2023-07-04)
==================================================================================================
## 🦖 Deprecations
* The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. ([\#3189](https://github.com/matrix-org/matrix-js-sdk/issues/3189)).
* ElementR: Add `CryptoApi#bootstrapSecretStorage` ([\#3483](https://github.com/matrix-org/matrix-js-sdk/pull/3483)). Contributed by @florianduros.
* Deprecate `MatrixClient.findVerificationRequestDMInProgress`, `MatrixClient.getVerificationRequestsToDeviceInProgress`, and `MatrixClient.requestVerification`, in favour of methods in `CryptoApi`. ([\#3474](https://github.com/matrix-org/matrix-js-sdk/pull/3474)).
* Introduce a new `Crypto.VerificationRequest` interface, and deprecate direct access to the old `VerificationRequest` class. Also deprecate some related classes that were exported from `src/crypto/verification/request/VerificationRequest` ([\#3449](https://github.com/matrix-org/matrix-js-sdk/pull/3449)).
## ✨ Features
* OIDC: navigate to authorization endpoint ([\#3499](https://github.com/matrix-org/matrix-js-sdk/pull/3499)). Contributed by @kerryarchibald.
* Support for interactive device verification in Element-R. ([\#3505](https://github.com/matrix-org/matrix-js-sdk/pull/3505)).
* Support for interactive device verification in Element-R. ([\#3508](https://github.com/matrix-org/matrix-js-sdk/pull/3508)).
* Support for interactive device verification in Element-R. ([\#3490](https://github.com/matrix-org/matrix-js-sdk/pull/3490)). Fixes vector-im/element-web#25316.
* Element-R: Store cross signing keys in secret storage ([\#3498](https://github.com/matrix-org/matrix-js-sdk/pull/3498)). Contributed by @florianduros.
* OIDC: add dynamic client registration util function ([\#3481](https://github.com/matrix-org/matrix-js-sdk/pull/3481)). Contributed by @kerryarchibald.
* Add getLastUnthreadedReceiptFor utility to Thread delegating to the underlying Room ([\#3493](https://github.com/matrix-org/matrix-js-sdk/pull/3493)).
* ElementR: Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation ([\#3472](https://github.com/matrix-org/matrix-js-sdk/pull/3472)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Aggregate relations regardless of whether event fits into the timeline ([\#3496](https://github.com/matrix-org/matrix-js-sdk/pull/3496)). Fixes vector-im/element-web#25596.
* Fix bug where switching media caused media in subsequent calls to fail ([\#3489](https://github.com/matrix-org/matrix-js-sdk/pull/3489)).
* Fix: remove polls from room state on redaction ([\#3475](https://github.com/matrix-org/matrix-js-sdk/pull/3475)). Fixes vector-im/element-web#25573. Contributed by @kerryarchibald.
* Fix export type `GeneratedSecretStorageKey` ([\#3479](https://github.com/matrix-org/matrix-js-sdk/pull/3479)). Contributed by @florianduros.
* Close IDB database before deleting it to prevent spurious unexpected close errors ([\#3478](https://github.com/matrix-org/matrix-js-sdk/pull/3478)). Fixes vector-im/element-web#25597.
Changes in [26.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.1.0) (2023-06-20)
==================================================================================================
## 🦖 Deprecations
* Introduce a new `Crypto.Verifier` interface, and deprecate direct access to `VerificationBase`, `SAS` and `ReciprocateQRCode` ([\#3414](https://github.com/matrix-org/matrix-js-sdk/pull/3414)).
## ✨ Features
* Add `rust-crypto#isCrossSigningReady` implementation ([\#3462](https://github.com/matrix-org/matrix-js-sdk/pull/3462)). Contributed by @florianduros.
* OIDC: Validate `m.authentication` configuration ([\#3419](https://github.com/matrix-org/matrix-js-sdk/pull/3419)). Contributed by @kerryarchibald.
* ElementR: Add `CryptoApi.getCrossSigningStatus` ([\#3452](https://github.com/matrix-org/matrix-js-sdk/pull/3452)). Contributed by @florianduros.
* Extend stats summary with call device and user count based on room state ([\#3424](https://github.com/matrix-org/matrix-js-sdk/pull/3424)). Contributed by @toger5.
* Update MSC3912 implementation to use `with_rel_type` instead of `with_relations` ([\#3420](https://github.com/matrix-org/matrix-js-sdk/pull/3420)).
* Export thread-related types from SDK ([\#3447](https://github.com/matrix-org/matrix-js-sdk/pull/3447)). Contributed by @stas-demydiuk.
* Use correct /v3 prefix for /refresh ([\#3016](https://github.com/matrix-org/matrix-js-sdk/pull/3016)). Contributed by @davidisaaclee.
## 🐛 Bug Fixes
* Fix thread list being ordered based on all updates ([\#3458](https://github.com/matrix-org/matrix-js-sdk/pull/3458)). Fixes vector-im/element-web#25522.
* Fix: handle `baseUrl` with trailing slash in `fetch.getUrl` ([\#3455](https://github.com/matrix-org/matrix-js-sdk/pull/3455)). Fixes vector-im/element-web#25526. Contributed by @kerryarchibald.
* use cli.canSupport to determine intentional mentions support ([\#3445](https://github.com/matrix-org/matrix-js-sdk/pull/3445)). Fixes vector-im/element-web#25497. Contributed by @kerryarchibald.
* Make sliding sync linearize processing of sync requests ([\#3442](https://github.com/matrix-org/matrix-js-sdk/pull/3442)).
* Fix edge cases around 2nd order relations and threads ([\#3437](https://github.com/matrix-org/matrix-js-sdk/pull/3437)).
Changes in [26.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.1) (2023-06-09)
==================================================================================================
## 🐛 Bug Fixes
* Fix: handle `baseUrl` with trailing slash in `fetch.getUrl` ([\#3455](https://github.com/matrix-org/matrix-js-sdk/pull/3455)). Fixes vector-im/element-web#25526. Contributed by @kerryarchibald.
Changes in [26.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.0) (2023-06-06)
==================================================================================================
## 🚨 BREAKING CHANGES
* Ensure we do not add relations to the wrong timeline ([\#3427](https://github.com/matrix-org/matrix-js-sdk/pull/3427)). Fixes vector-im/element-web#25450 and vector-im/element-web#25494.
* Deprecate `QrCodeEvent`, `SasEvent` and `VerificationEvent` ([\#3386](https://github.com/matrix-org/matrix-js-sdk/pull/3386)).
## 🦖 Deprecations
* Move crypto classes into a separate namespace ([\#3385](https://github.com/matrix-org/matrix-js-sdk/pull/3385)).
## ✨ Features
* Mention deno support in the README ([\#3417](https://github.com/matrix-org/matrix-js-sdk/pull/3417)). Contributed by @sigmaSd.
* Mark room version 10 as safe ([\#3425](https://github.com/matrix-org/matrix-js-sdk/pull/3425)).
* Prioritise entirely supported flows for UIA ([\#3402](https://github.com/matrix-org/matrix-js-sdk/pull/3402)).
* Add methods to terminate idb worker ([\#3362](https://github.com/matrix-org/matrix-js-sdk/pull/3362)).
* Total summary count ([\#3351](https://github.com/matrix-org/matrix-js-sdk/pull/3351)). Contributed by @toger5.
* Audio concealment ([\#3349](https://github.com/matrix-org/matrix-js-sdk/pull/3349)). Contributed by @toger5.
@@ -13,6 +202,11 @@ Changes in [25.2.0-rc.4](https://github.com/matrix-org/matrix-js-sdk/releases/ta
* Keep measuring a call feed's volume after a stream replacement ([\#3361](https://github.com/matrix-org/matrix-js-sdk/pull/3361)). Fixes vector-im/element-call#1051.
* Element-R: Avoid uploading a new fallback key at every `/sync` ([\#3338](https://github.com/matrix-org/matrix-js-sdk/pull/3338)). Fixes vector-im/element-web#25215.
* Accumulate receipts for the main thread and unthreaded separately ([\#3339](https://github.com/matrix-org/matrix-js-sdk/pull/3339)). Fixes vector-im/element-web#24629.
* Remove spec non-compliant extended glob format ([\#3423](https://github.com/matrix-org/matrix-js-sdk/pull/3423)). Fixes vector-im/element-web#25474.
* Fix bug where original event was inserted into timeline instead of the edit event ([\#3398](https://github.com/matrix-org/matrix-js-sdk/pull/3398)). Contributed by @andybalaam.
* Only add a local receipt if it's after an existing receipt ([\#3399](https://github.com/matrix-org/matrix-js-sdk/pull/3399)). Contributed by @andybalaam.
* Attempt a potential workaround for stuck notifs ([\#3384](https://github.com/matrix-org/matrix-js-sdk/pull/3384)). Fixes vector-im/element-web#25406. Contributed by @andybalaam.
* Fix verification bug with `pendingEventOrdering: "chronological"` ([\#3382](https://github.com/matrix-org/matrix-js-sdk/pull/3382)).
Changes in [25.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.1) (2023-05-16)
==================================================================================================
+6
View File
@@ -11,6 +11,8 @@
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
browser or in Node.js.
#### Minimum Matrix server version: v1.1
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports
is removed in v1.4 then the feature is _eligible_ for removal from the SDK when v1.8 is released. This SDK has no
@@ -21,6 +23,8 @@ endpoints from before Matrix 1.1, for example.
## In a browser
### Note, the browserify build has been deprecated. Please use a bundler like webpack or vite instead.
Download the browser version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
`<script>` to your page. There will be a global variable `matrixcs`
@@ -55,6 +59,8 @@ client.publicRooms(function (err, data) {
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node) for a more complex example.
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
To start the client:
```javascript
+9 -1
View File
@@ -23,11 +23,19 @@ const config: Config = {
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts}"],
coverageReporters: ["text-summary", "lcov"],
testResultsProcessor: "@casualbot/jest-sonar-reporter",
// Always print out a summary if there are any failing tests. Normally
// a summary is only printed if there are more than 20 test *suites*.
reporters: [["default", { summaryThreshold: 0 }]],
};
// if we're running under GHA, enable the GHA reporter
if (env["GITHUB_ACTIONS"] !== undefined) {
const reporters: Config["reporters"] = [["github-actions", { silent: false }], "summary"];
const reporters: Config["reporters"] = [
["github-actions", { silent: false }],
// as above: always show a summary if there were any failing tests.
["summary", { summaryThreshold: 0 }],
];
// if we're running against the develop branch, also enable the slow test reporter
if (env["GITHUB_REF"] == "refs/heads/develop") {
+11 -10
View File
@@ -1,9 +1,9 @@
{
"name": "matrix-js-sdk",
"version": "25.2.0-rc.4",
"version": "28.2.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
"node": ">=18.0.0"
},
"scripts": {
"prepublishOnly": "yarn build",
@@ -55,13 +55,15 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.9",
"@matrix-org/matrix-sdk-crypto-wasm": "^1.2.3-alpha.0",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"jwt-decode": "^3.1.2",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.3.1",
"matrix-widget-api": "^1.6.0",
"oidc-client-ts": "^2.2.4",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
@@ -95,22 +97,20 @@
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"browserify-swap": "^0.2.2",
"debug": "^4.3.4",
"docdash": "^2.0.0",
"domexception": "^4.0.0",
"eslint": "8.39.0",
"eslint": "8.48.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-jsdoc": "^43.0.6",
"eslint-plugin-jsdoc": "^46.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^46.0.0",
"eslint-plugin-unicorn": "^48.0.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"fetch-mock-jest": "^1.5.1",
@@ -125,6 +125,7 @@
"ts-node": "^10.9.1",
"tsify": "^5.0.2",
"typedoc": "^0.24.0",
"typedoc-plugin-coverage": "^2.1.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^2.0.0",
"typedoc-plugin-versions": "^0.2.3",
+1 -18
View File
@@ -32,8 +32,6 @@ import { syncPromise } from "./test-utils/test-utils";
import { createClient, IStartClientOpts } from "../src/matrix";
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils";
import { IKeyBackupSession } from "../src/crypto/keybackup";
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
import { ISyncResponder } from "./test-utils/SyncResponder";
@@ -92,7 +90,7 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
logger.log(this + ": starting");
this.httpBackend.when("GET", "/versions").respond(200, {
// we have tests that rely on support for lazy-loading members
versions: ["r0.5.0"],
versions: ["v1.1"],
});
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
@@ -214,21 +212,6 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
});
}
/**
* 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
*
-117
View File
@@ -1,117 +0,0 @@
/*
Copyright 2023 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 fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, InitCrypto } from "../test-utils/test-utils";
import { createClient, MatrixClient, UIAuthCallback } from "../../src";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
const TEST_USER_ID = "@alice:localhost";
const TEST_DEVICE_ID = "xzcvb";
/**
* Integration tests for cross-signing functionality.
*
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
let aliceClient: MatrixClient;
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
});
await initCrypto(aliceClient);
});
afterEach(async () => {
await aliceClient.stopClient();
fetchMock.mockReset();
});
describe("bootstrapCrossSigning (before initialsync completes)", () => {
oldBackendOnly("publishes keys if none were yet published", async () => {
// have account_data requests return an empty object
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// ... and one to upload the cross-signing keys (with UIA)
fetchMock.post(
{ url: "path:/_matrix/client/unstable/keys/device_signing/upload", name: "upload-keys" },
{},
);
// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
await makeRequest(authDict);
};
// now bootstrap cross signing, and check it resolves successfully
await aliceClient.bootstrapCrossSigning({
authUploadDeviceSigningKeys: uiaCallback,
});
// check the cross-signing keys upload
expect(fetchMock.called("upload-keys")).toBeTruthy();
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
const keysBody = JSON.parse(keysOpts!.body as string);
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
// there should be a key of each type
// master key is signed by the device
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
// ssk and usk are signed by the master key
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
// check the publish call
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
const body = JSON.parse(sigsOpts!.body as string);
// there should be a signature for our device, by our self-signing key.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
);
});
});
});
+342
View File
@@ -0,0 +1,342 @@
/*
Copyright 2023 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 fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { encryptAES } from "../../../src/crypto/aes";
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import {
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64,
SIGNED_CROSS_SIGNING_KEYS_DATA,
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
} from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
const TEST_USER_ID = "@alice:localhost";
const TEST_DEVICE_ID = "xzcvb";
/**
* Integration tests for cross-signing functionality.
*
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
// backend. Once we drop support for legacy crypto, it will go away.
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: ISyncResponder;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
// Encryption key used to encrypt cross signing keys
const encryptionKey = new Uint8Array(32);
/**
* Create the {@link CryptoCallbacks}
*/
function createCryptoCallbacks(): CryptoCallbacks {
return {
getSecretStorageKey: (keys, name) => {
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
},
};
}
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
cryptoCallbacks: createCryptoCallbacks(),
});
syncResponder = new SyncResponder(homeserverUrl);
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
/** an object which intercepts `/keys/upload` requests on the test homeserver */
new E2EKeyReceiver(homeserverUrl);
await initCrypto(aliceClient);
});
afterEach(async () => {
await aliceClient.stopClient();
fetchMock.mockReset();
});
/**
* Create cross-signing keys and publish the keys
*
* @param authDict - The parameters to as the `auth` dict in the key upload request.
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
*/
async function bootstrapCrossSigning(authDict: AuthDict): Promise<void> {
await aliceClient.getCrypto()?.bootstrapCrossSigning({
authUploadDeviceSigningKeys: (makeRequest) => makeRequest(authDict).then(() => undefined),
});
}
describe("bootstrapCrossSigning (before initialsync completes)", () => {
it("publishes keys if none were yet published", async () => {
mockSetupCrossSigningRequests();
// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// check the cross-signing keys upload
expect(fetchMock.called("upload-keys")).toBeTruthy();
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
const keysBody = JSON.parse(keysOpts!.body as string);
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
// there should be a key of each type
// master key is signed by the device
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
// ssk and usk are signed by the master key
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
// check the publish call
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
const body = JSON.parse(sigsOpts!.body as string);
// there should be a signature for our device, by our self-signing key.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
);
});
newBackendOnly("get cross signing keys from secret storage and import them", async () => {
// Return public cross signing keys
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
const masterKey = await encryptAES(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.master",
);
const selfSigningKey = await encryptAES(
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.self_signing",
);
const userSigningKey = await encryptAES(
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.user_signing",
);
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.cross_signing.master",
content: {
encrypted: {
key_id: masterKey,
},
},
},
{
type: "m.cross_signing.self_signing",
content: {
encrypted: {
key_id: selfSigningKey,
},
},
},
{
type: "m.cross_signing.user_signing",
content: {
encrypted: {
key_id: userSigningKey,
},
},
},
{
type: "m.secret_storage.key.key_id",
content: {
key: "key_id",
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
],
},
});
await aliceClient.startClient();
await syncPromise(aliceClient);
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
);
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// Check if the UserTrustStatusChanged event was fired
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
// Expect the signature to be uploaded
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
const body = JSON.parse(sigsOpts!.body as string);
// the device should have a signature with the public self cross signing keys.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
);
});
});
describe("getCrossSigningStatus()", () => {
it("should return correct values without bootstrapping cross-signing", async () => {
mockSetupCrossSigningRequests();
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
// Expect the cross signing keys to be unavailable
expect(crossSigningStatus).toStrictEqual({
publicKeysOnDevice: false,
privateKeysInSecretStorage: false,
privateKeysCachedLocally: { masterKey: false, userSigningKey: false, selfSigningKey: false },
});
});
it("should return correct values after bootstrapping cross-signing", async () => {
mockSetupCrossSigningRequests();
// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
// Expect the cross signing keys to be available
expect(crossSigningStatus).toStrictEqual({
publicKeysOnDevice: true,
privateKeysInSecretStorage: false,
privateKeysCachedLocally: { masterKey: true, userSigningKey: true, selfSigningKey: true },
});
});
});
describe("isCrossSigningReady()", () => {
it("should return false if cross-signing is not bootstrapped", async () => {
mockSetupCrossSigningRequests();
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeFalsy();
});
it("should return true after bootstrapping cross-signing", async () => {
mockSetupCrossSigningRequests();
await bootstrapCrossSigning({ type: "test" });
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeTruthy();
});
});
describe("getCrossSigningKeyId", () => {
/**
* Intercept /keys/device_signing/upload request and return the cross signing keys
* https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3keysdevice_signingupload
*
* @returns the cross signing keys
*/
function awaitCrossSigningKeysUpload() {
return new Promise<any>((resolve) => {
fetchMock.post(
// legacy crypto uses /unstable/; /v3/ is correct
{
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
name: "upload-keys",
},
(url, options) => {
const content = JSON.parse(options.body as string);
resolve(content);
return {};
},
// Override the routes define in `mockSetupCrossSigningRequests`
{ overwriteRoutes: true },
);
});
}
it("should return the cross signing key id for each cross signing key", async () => {
mockSetupCrossSigningRequests();
// Intercept cross signing keys upload
const crossSigningKeysPromise = awaitCrossSigningKeysUpload();
// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// Get the cross signing keys
const crossSigningKeys = await crossSigningKeysPromise;
const getPubKey = (crossSigningKey: any) => Object.values(crossSigningKey!.keys)[0];
const masterKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId();
expect(masterKeyId).toBe(getPubKey(crossSigningKeys.master_key));
const selfSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.SelfSigning);
expect(selfSigningKeyId).toBe(getPubKey(crossSigningKeys.self_signing_key));
const userSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.UserSigning);
expect(userSigningKeyId).toBe(getPubKey(crossSigningKeys.user_signing_key));
});
});
});
File diff suppressed because it is too large Load Diff
+799
View File
@@ -0,0 +1,799 @@
/*
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 fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient, TypedEventEmitter } from "../../../src";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { awaitDecryption, CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import * as testData from "../../test-utils/test-data";
import { KeyBackupInfo } from "../../../src/crypto-api/keybackup";
import { IKeyBackup } from "../../../src/crypto/backup";
const ROOM_ID = testData.TEST_ROOM_ID;
/** The homeserver url that we give to the test client, and where we intercept /sync, /keys, etc requests. */
const TEST_HOMESERVER_URL = "https://alice-server.com";
const TEST_USER_ID = "@alice:localhost";
const TEST_DEVICE_ID = "xzcvb";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
enum MockKeyUploadEvent {
KeyUploaded = "KeyUploaded",
}
type MockKeyUploadEventHandlerMap = {
[MockKeyUploadEvent.KeyUploaded]: (roomId: string, sessionId: string, backupVersion: string) => void;
};
/*
* Test helper. Returns an event emitter that will emit an event every time fetchmock sees a request to backup a key.
*/
function mockUploadEmitter(
expectedVersion: string,
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
const emitter = new TypedEventEmitter();
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const version = new URLSearchParams(new URL(url).search).get("version");
if (version != expectedVersion) {
return {
status: 403,
body: {
current_version: expectedVersion,
errcode: "M_WRONG_ROOM_KEYS_VERSION",
error: "Wrong backup version.",
},
};
}
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) {
emitter.emit(MockKeyUploadEvent.KeyUploaded, roomId, sessionId, version);
count++;
}
}
return {
status: 200,
body: {
count: count,
etag: "abcdefg",
},
};
},
{
overwriteRoutes: true,
},
);
return emitter;
}
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
// const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
// const newBackendOnly = backend === "libolm" ? test.skip : test;
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests on the test homeserver */
let syncResponder: SyncResponder;
/** an object which intercepts `/keys/upload` requests on the test homeserver */
let e2eKeyReceiver: E2EKeyReceiver;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
jest.useFakeTimers();
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
mockInitialApiRequests(TEST_HOMESERVER_URL);
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
e2eKeyResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA);
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
});
afterEach(async () => {
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
});
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
const client = createClient({
baseUrl: TEST_HOMESERVER_URL,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
...opts,
});
await initCrypto(client);
return client;
}
it("Alice checks key backups when receiving a message she can't decrypt", async function () {
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [testData.ENCRYPTED_EVENT],
},
},
},
},
};
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"));
// start after saving the private key
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
// XXX: should we automatically re-check after a device becomes verified?
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
// Now, send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
await awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
});
describe("recover from backup", () => {
it("can restore from backup (Curve25519 version)", async function () {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
const fullBackup = {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
const check = await aliceCrypto.checkKeyBackupAndEnable();
let onKeyCached: () => void;
const awaitKeyCached = new Promise<void>((resolve) => {
onKeyCached = resolve;
});
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
);
expect(result.imported).toStrictEqual(1);
await awaitKeyCached;
});
it("recover specific session from backup", async function () {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
);
const check = await aliceCrypto.checkKeyBackupAndEnable();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
ROOM_ID,
testData.MEGOLM_SESSION_DATA.session_id,
check!.backupInfo!,
);
expect(result.imported).toStrictEqual(1);
});
it("Fails on bad recovery key", async function () {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
const fullBackup = {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
const check = await aliceCrypto.checkKeyBackupAndEnable();
await expect(
aliceClient.restoreKeyBackupWithRecoveryKey(
"EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD",
undefined,
undefined,
check!.backupInfo!,
),
).rejects.toThrow();
});
});
describe("backupLoop", () => {
it("Alice should upload known keys when backup is enabled", async function () {
// 404 means that there is no active backup
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
// check that signalling is working
const remainingZeroPromise = new Promise<void>((resolve, reject) => {
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
if (remaining == 0) {
resolve();
}
});
});
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
const uploadMockEmitter = mockUploadEmitter(testData.SIGNED_BACKUP_DATA.version!);
const uploadPromises = someRoomKeys.map((data) => {
new Promise<void>((resolve) => {
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
if (
data.room_id == roomId &&
data.session_id == sessionId &&
version == testData.SIGNED_BACKUP_DATA.version
) {
resolve();
}
});
});
});
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
await Promise.all(uploadPromises);
// Wait until all keys are backed up to ensure that when a new key is received the loop is restarted
await remainingZeroPromise;
// A new key import should trigger a new upload.
const newKey = testData.MEGOLM_SESSION_DATA;
const newKeyUploadPromise = new Promise<void>((resolve) => {
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
if (
newKey.room_id == roomId &&
newKey.session_id == sessionId &&
version == testData.SIGNED_BACKUP_DATA.version
) {
resolve();
}
});
});
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
await newKeyUploadPromise;
});
it("Alice should re-upload all keys if a new trusted backup is available", async function () {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
// check that signalling is working
const remainingZeroPromise = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
if (remaining == 0) {
resolve();
}
});
});
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
mockUploadEmitter(testData.SIGNED_BACKUP_DATA.version!);
await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
// wait for all keys to be backed up
await remainingZeroPromise;
const newBackupVersion = "2";
const uploadMockEmitter = mockUploadEmitter(newBackupVersion);
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
newBackup.version = newBackupVersion;
// Let's simulate that a new backup is available by returning error code on key upload
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
overwriteRoutes: true,
});
// If we import a new key the loop will try to upload to old version, it will
// fail then check the current version and switch if trusted
const uploadPromises = someRoomKeys.map((data) => {
new Promise<void>((resolve) => {
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
if (data.room_id == roomId && data.session_id == sessionId && version == newBackupVersion) {
resolve();
}
});
});
});
const disableOldBackup = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupFailed, (errCode) => {
if (errCode == "M_WRONG_ROOM_KEYS_VERSION") {
resolve();
}
});
});
const enableNewBackup = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});
// A new key import should trigger a new upload.
const newKey = testData.MEGOLM_SESSION_DATA;
const newKeyUploadPromise = new Promise<void>((resolve) => {
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
if (newKey.room_id == roomId && newKey.session_id == sessionId && version == newBackupVersion) {
resolve();
}
});
});
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
await disableOldBackup;
await enableNewBackup;
jest.runAllTimers();
await Promise.all(uploadPromises);
await newKeyUploadPromise;
});
it("Backup loop should be resistant to network failures", async function () {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
// on the first key upload attempt, simulate a network failure
const failurePromise = new Promise((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
() => {
resolve(undefined);
throw new TypeError(`Failed to fetch`);
},
{
overwriteRoutes: true,
},
);
});
// kick the import loop off and wait for the failed request
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
await aliceCrypto.importRoomKeys(someRoomKeys);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
jest.runAllTimers();
await failurePromise;
// Fix the endpoint to do successful uploads
const successPromise = new Promise((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
() => {
resolve(undefined);
return {
status: 200,
body: {
count: 2,
etag: "abcdefg",
},
};
},
{
overwriteRoutes: true,
},
);
});
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
const allKeysUploadedPromise = new Promise((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
if (remaining == 0) {
resolve(undefined);
}
});
});
// run the timers, which will make the backup loop redo the request
await jest.runAllTimersAsync();
await successPromise;
await allKeysUploadedPromise;
});
});
it("getActiveSessionBackupVersion() should give correct result", async function () {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceCrypto.checkKeyBackupAndEnable();
// At this point there is no backup
let backupStatus: string | null;
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toBeNull();
// Serve a backup with no trusted signature
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
delete unsignedBackup.auth_data.signatures;
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
const checked = await aliceCrypto.checkKeyBackupAndEnable();
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
expect(checked?.trustInfo?.trusted).toBeFalsy();
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toBeNull();
// Add a valid signature to the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
// check that signalling is working
const backupPromise = new Promise<void>((resolve, reject) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});
const validCheck = await aliceCrypto.checkKeyBackupAndEnable();
expect(validCheck?.trustInfo?.trusted).toStrictEqual(true);
await backupPromise;
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
});
describe("isKeyBackupTrusted", () => {
it("does not trust a backup signed by an untrusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// download the device list, to match the trusted case
await aliceClient.startClient();
await waitForDeviceList();
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
expect(result).toEqual({ trusted: false, matchesDecryptionKey: false });
});
it("trusts a backup signed by a trusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
expect(result).toEqual({ trusted: true, matchesDecryptionKey: false });
});
it("recognises a backup which matches the decryption key", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
);
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
expect(result).toEqual({ trusted: false, matchesDecryptionKey: true });
});
it("is not fooled by a backup which matches the decryption key but uses a different algorithm", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
);
const backup: KeyBackupInfo = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
backup.algorithm = "m.megolm_backup.v1.aes-hmac-sha2";
const result = await aliceCrypto.isKeyBackupTrusted(backup);
expect(result).toEqual({ trusted: false, matchesDecryptionKey: false });
});
});
describe("checkKeyBackupAndEnable", () => {
it("enables a backup signed by a trusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(result!.trustInfo).toEqual({ trusted: true, matchesDecryptionKey: false });
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
});
it("does not enable a backup signed by an untrusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// download the device list, to match the trusted case
await aliceClient.startClient();
await waitForDeviceList();
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(result!.trustInfo).toEqual({ trusted: false, matchesDecryptionKey: false });
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
});
it("disables backup when a new untrusted backup is available", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
delete unsignedBackup.auth_data.signatures;
unsignedBackup.version = "2";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
});
it("switches backup when a new trusted backup is available", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
const newBackupVersion = "2";
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
newBackup.version = newBackupVersion;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
overwriteRoutes: true,
});
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
});
it("Disables when backup is deleted", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
},
{
overwriteRoutes: true,
},
);
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
expect(noResult).toBeNull();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
});
});
/** make sure that the client knows about the dummy device */
async function waitForDeviceList(): Promise<void> {
// Completing the initial sync will make the device list download outdated device lists (of which our own
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID);
}
});
@@ -26,16 +26,16 @@ limitations under the License.
*/
// load olm before the sdk if possible
import "../olm-loader";
import "../../olm-loader";
import type { Session } from "@matrix-org/olm";
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
import { logger } from "../../src/logger";
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import type { IDeviceKeys, IOneTimeKey } from "../../../src/@types/crypto";
import { logger } from "../../../src/logger";
import * as testUtils from "../../test-utils/test-utils";
import { TestClient } from "../../TestClient";
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, 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";
@@ -17,7 +17,7 @@ limitations under the License.
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { createClient } from "../../src";
import { createClient } from "../../../src";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -87,4 +87,19 @@ describe("MatrixClient.clearStores", () => {
await matrixClient.clearStores();
expect(await indexedDB.databases()).toHaveLength(0);
});
it("should not fail in environments without indexedDB", async () => {
// eslint-disable-next-line no-global-assign
indexedDB = undefined!;
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
await matrixClient.stopClient();
await matrixClient.clearStores();
// No error thrown in clearStores
});
});
File diff suppressed because it is too large Load Diff
@@ -92,9 +92,7 @@ describe("MatrixClient events", function () {
type: "m.room.create",
room: "!erufh:bar",
user: "@foo:bar",
content: {
creator: "@foo:bar",
},
content: {},
}),
],
},
+169 -105
View File
@@ -107,9 +107,7 @@ const INITIAL_SYNC_DATA = {
utils.mkEvent({
type: "m.room.create",
user: userId,
content: {
creator: userId,
},
content: {},
event: false,
}),
],
@@ -207,7 +205,7 @@ function startClient(httpBackend: HttpBackend, client: MatrixClient) {
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
client.startClient();
client.startClient({ threadSupport: true });
// set up a promise which will resolve once the client is initialised
const prom = new Promise<void>((resolve) => {
@@ -248,7 +246,7 @@ describe("getEventTimeline support", function () {
return startClient(httpBackend, client).then(function () {
const room = client.getRoom(roomId)!;
const timelineSet = room!.getTimelineSets()[0];
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
return expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
});
});
@@ -260,7 +258,18 @@ describe("getEventTimeline support", function () {
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId)!;
const timelineSet = room!.getTimelineSets()[0];
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeFalsy();
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/context/event`).respond(200, () => ({
event: {
event_id: "event",
},
events_after: [],
events_before: [],
state: [],
}));
return Promise.all([
expect(client.getEventTimeline(timelineSet, "event")).resolves.toBeTruthy(),
httpBackend.flushAllExpected(),
]);
});
});
@@ -271,7 +280,7 @@ describe("getEventTimeline support", function () {
return startClient(httpBackend, client).then(function () {
const timelineSet = new EventTimelineSet(undefined);
expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
return expect(client.getEventTimeline(timelineSet, "event")).rejects.toBeTruthy();
});
});
@@ -598,12 +607,6 @@ describe("MatrixClient event timelines", function () {
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
@@ -634,12 +637,6 @@ describe("MatrixClient event timelines", function () {
const thread = room.createThread(THREAD_ROOT.event_id!, undefined, [], false);
await httpBackend.flushAllExpected();
const timelineSet = thread.timelineSet;
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
await flushHttp(emitPromise(thread, ThreadEvent.Update));
const timeline = await client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!);
@@ -790,7 +787,18 @@ describe("MatrixClient event timelines", function () {
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId)!;
const timelineSet = room.getTimelineSets()[0];
expect(client.getLatestTimeline(timelineSet)).rejects.toBeFalsy();
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/context/event`).respond(200, () => ({
event: {
event_id: "event",
},
events_after: [],
events_before: [],
state: [],
}));
return Promise.all([
expect(client.getEventTimeline(timelineSet, "event")).resolves.toBeTruthy(),
httpBackend.flushAllExpected(),
]);
});
});
@@ -1142,7 +1150,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog
room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2]);
httpBackend
.when(
"GET",
@@ -1156,7 +1164,7 @@ describe("MatrixClient event timelines", function () {
});
await flushHttp(prom);
// but while loading the metadata, a new reply has arrived
room.addLiveEvents([THREAD_REPLY3]);
await room.addLiveEvents([THREAD_REPLY3]);
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([
@@ -1248,7 +1256,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog
room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2]);
httpBackend
.when(
"GET",
@@ -1267,7 +1275,7 @@ describe("MatrixClient event timelines", function () {
});
await flushHttp(prom);
// but while loading the metadata, a new reply has arrived
room.addLiveEvents([THREAD_REPLY3]);
await room.addLiveEvents([THREAD_REPLY3]);
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([
@@ -1322,7 +1330,7 @@ describe("MatrixClient event timelines", function () {
request.respond(200, function () {
return {
original_event: root,
chunk: [replies],
chunk: replies,
// no next batch as this is the oldest end of the timeline
};
});
@@ -1332,7 +1340,7 @@ describe("MatrixClient event timelines", function () {
function respondToContext(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
const request = httpBackend.when(
"GET",
encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", {
encodeUri("/_matrix/client/v3/rooms/$roomId/context/$eventId", {
$roomId: roomId,
$eventId: event.event_id!,
}),
@@ -1350,7 +1358,7 @@ describe("MatrixClient event timelines", function () {
function respondToEvent(event: Partial<IEvent> = THREAD_ROOT): ExpectedHttpRequest {
const request = httpBackend.when(
"GET",
encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", {
encodeUri("/_matrix/client/v3/rooms/$roomId/event/$eventId", {
$roomId: roomId,
$eventId: event.event_id!,
}),
@@ -1361,7 +1369,7 @@ describe("MatrixClient event timelines", function () {
function respondToMessagesRequest(): ExpectedHttpRequest {
const request = httpBackend.when(
"GET",
encodeUri("/_matrix/client/r0/rooms/$roomId/messages", {
encodeUri("/_matrix/client/v3/rooms/$roomId/messages", {
$roomId: roomId,
}),
);
@@ -1452,7 +1460,7 @@ describe("MatrixClient event timelines", function () {
expect(room.getPendingEvents()).toHaveLength(1);
});
it("should handle thread updates by reordering the thread list", async () => {
it("should handle new thread replies by reordering the thread list", async () => {
// Test data for a second thread
const THREAD2_ROOT = utils.mkEvent({
room: roomId,
@@ -1479,7 +1487,7 @@ describe("MatrixClient event timelines", function () {
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"body": "thread2 reply",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
@@ -1499,7 +1507,7 @@ describe("MatrixClient event timelines", function () {
user: userId,
type: "m.room.message",
content: {
"body": "thread reply",
"body": "thread reply2",
"msgtype": "m.text",
"m.relates_to": {
// We can't use the const here because we change server support mode for test
@@ -1509,7 +1517,8 @@ describe("MatrixClient event timelines", function () {
},
event: true,
});
THREAD_REPLY2.localTimestamp += 1000;
// this has to come after THREAD_REPLY which hasn't been instantiated by us
THREAD_REPLY2.localTimestamp += 10000000;
// Test data for the first thread, with the second reply
const THREAD_ROOT_UPDATED = {
@@ -1569,10 +1578,8 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.NewReply);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD2_ROOT);
room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2]);
await httpBackend.flushAllExpected();
await prom;
expect(thread.length).toBe(2);
@@ -1582,6 +1589,132 @@ describe("MatrixClient event timelines", function () {
THREAD_ROOT.event_id,
]);
});
it("should not reorder the thread list on other thread updates", async () => {
// Test data for a second thread
const THREAD2_ROOT = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
body: "thread root",
msgtype: "m.text",
},
unsigned: {
"m.relations": {
"io.element.thread": {
//"latest_event": undefined,
count: 1,
current_user_participated: true,
},
},
},
event: false,
});
const THREAD2_REPLY = utils.mkEvent({
room: roomId,
user: userId,
type: "m.room.message",
content: {
"body": "thread2 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,
},
},
event: false,
});
// @ts-ignore we know this is a defined path for THREAD ROOT
THREAD2_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD2_REPLY;
const THREAD_REPLY_REACTION = utils.mkEvent({
room: roomId,
user: userId,
type: "m.reaction",
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: THREAD_REPLY.event_id,
key: "🪿",
},
},
event: true,
});
THREAD_REPLY_REACTION.localTimestamp += 1000;
// Modified thread root event containing latest thread reply in its unsigned
const THREAD_ROOT_UPDATED = {
...THREAD_ROOT,
unsigned: {
...THREAD_ROOT.unsigned,
"m.relations": {
...THREAD_ROOT.unsigned!["m.relations"],
"io.element.thread": {
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
count: 2,
latest_event: THREAD_REPLY,
},
},
},
};
// Response with test data for the thread list request
const threadsResponse = {
chunk: [THREAD2_ROOT, THREAD_ROOT],
state: [],
next_batch: RANDOM_TOKEN as string | null,
};
// @ts-ignore
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
// Set up room threads
const timelineSets = await room!.createThreadsTimelineSets();
expect(timelineSets).not.toBeNull();
respondToThreads(threadsResponse);
respondToThreads(threadsResponse);
respondToEvent(THREAD_ROOT);
respondToEvent(THREAD2_ROOT);
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
await flushHttp(room.fetchRoomThreads());
const threadIds = room.getThreads().map((thread) => thread.id);
expect(threadIds).toContain(THREAD_ROOT.event_id);
expect(threadIds).toContain(THREAD2_ROOT.event_id);
const [allThreads] = timelineSets!;
const timeline = allThreads.getLiveTimeline()!;
// Test threads are in chronological order
expect(timeline.getEvents().map((it) => it.event.event_id)).toEqual([
THREAD_ROOT.event_id,
THREAD2_ROOT.event_id,
]);
// Test adding a second event to the first thread
const thread = room.getThread(THREAD_ROOT.event_id!)!;
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.Update);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD2_ROOT);
await room.addLiveEvents([THREAD_REPLY_REACTION]);
await httpBackend.flushAllExpected();
await prom;
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread
// Test thread order is unchanged
expect(timeline!.getEvents().map((it) => it.event.event_id)).toEqual([
THREAD_ROOT.event_id,
THREAD2_ROOT.event_id,
]);
});
});
describe("without server compatibility", function () {
@@ -1917,76 +2050,7 @@ describe("MatrixClient event timelines", function () {
expect(thread.initialEventsFetched).toBeTruthy();
const timelineSet = thread.timelineSet;
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return {
start: "start_token",
events_before: [],
event: THREAD_ROOT,
events_after: [],
end: "end_token",
state: [],
};
});
httpBackend
.when(
"GET",
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) +
"/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) +
buildRelationPaginationQuery({
dir: Direction.Backward,
from: "start_token",
}),
)
.respond(200, function () {
return {
chunk: [],
};
});
httpBackend
.when(
"GET",
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
encodeURIComponent(THREAD_ROOT.event_id!) +
"/" +
encodeURIComponent(THREAD_RELATION_TYPE.name) +
buildRelationPaginationQuery({ dir: Direction.Forward, from: "end_token" }),
)
.respond(200, function () {
return {
chunk: [THREAD_REPLY],
};
});
const timeline = await flushHttp(client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!));
const timeline = await client.getEventTimeline(timelineSet, THREAD_ROOT.event_id!);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -57,9 +57,7 @@ describe("MatrixClient opts", function () {
type: "m.room.create",
room: roomId,
user: userId,
content: {
creator: userId,
},
content: {},
}),
],
},
@@ -85,9 +85,7 @@ describe("MatrixClient room timelines", function () {
type: "m.room.create",
room: roomId,
user: userId,
content: {
creator: userId,
},
content: {},
}),
],
},
+346 -21
View File
@@ -36,11 +36,16 @@ import {
NotificationCountType,
IEphemeral,
Room,
IndexedDBStore,
RelationType,
EventType,
} from "../../src";
import { ReceiptType } from "../../src/@types/read_receipts";
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
describe("MatrixClient syncing", () => {
const selfUserId = "@alice:localhost";
@@ -218,9 +223,122 @@ describe("MatrixClient syncing", () => {
expect(fires).toBe(3);
});
it("should honour lazyLoadMembers if user is not a guest", () => {
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
await client!.initCrypto();
const roomId = "!cycles:example.org";
// First sync: an knock
const knockSyncRoomSection = {
knock: {
[roomId]: {
knock_state: {
events: [
{
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "knock",
},
},
],
},
},
},
};
httpBackend!.when("GET", "/sync").respond(200, {
...syncData,
rooms: knockSyncRoomSection,
});
// Second sync: a leave (reject of some kind)
httpBackend!.when("POST", "/leave").respond(200, {});
httpBackend!.when("GET", "/sync").respond(200, {
...syncData,
rooms: {
leave: {
[roomId]: {
account_data: { events: [] },
ephemeral: { events: [] },
state: {
events: [
{
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
},
prev_content: {
membership: "knock",
},
// XXX: And other fields required on an event
},
],
},
timeline: {
limited: false,
events: [
{
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
},
prev_content: {
membership: "knock",
},
// XXX: And other fields required on an event
},
],
},
},
},
},
});
// Third sync: another knock
httpBackend!.when("GET", "/sync").respond(200, {
...syncData,
rooms: knockSyncRoomSection,
});
// First fire: an initial knock
let fires = 0;
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
// Room, string, string
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("knock");
expect(oldMembership).toBeFalsy();
// Second fire: a leave
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("leave");
expect(oldMembership).toBe("knock");
// Third/final fire: a second knock
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("knock");
expect(oldMembership).toBe("leave");
});
});
// For maximum safety, "leave" the room after we register the handler
client!.leave(roomId);
});
// noinspection ES6MissingAwait
client!.startClient();
await httpBackend!.flushAllExpected();
expect(fires).toBe(3);
});
it("should honour lazyLoadMembers if user is not a guest", () => {
httpBackend!
.when("GET", "/sync")
.check((req) => {
@@ -237,8 +355,6 @@ describe("MatrixClient syncing", () => {
it("should not honour lazyLoadMembers if user is a guest", () => {
httpBackend!.expectedRequests = [];
httpBackend!.when("GET", "/versions").respond(200, {});
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
httpBackend!
.when("GET", "/sync")
.check((req) => {
@@ -292,6 +408,46 @@ describe("MatrixClient syncing", () => {
expect(fires).toBe(1);
});
it("should emit ClientEvent.Room when knocked while crypto is disabled", async () => {
const roomId = "!knock:example.org";
// First sync: a knock
const knockSyncRoomSection = {
knock: {
[roomId]: {
knock_state: {
events: [
{
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "knock",
},
},
],
},
},
},
};
httpBackend!.when("GET", "/sync").respond(200, {
...syncData,
rooms: knockSyncRoomSection,
});
// First fire: an initial knock
let fires = 0;
client!.once(ClientEvent.Room, (room) => {
fires++;
expect(room.roomId).toBe(roomId);
});
// noinspection ES6MissingAwait
client!.startClient();
await httpBackend!.flushAllExpected();
expect(fires).toBe(1);
});
it("should work when all network calls fail", async () => {
httpBackend!.expectedRequests = [];
httpBackend!.when("GET", "").fail(0, new Error("CORS or something"));
@@ -357,6 +513,7 @@ describe("MatrixClient syncing", () => {
join: {},
invite: {},
leave: {},
knock: {},
},
};
@@ -388,9 +545,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.create",
room: roomOne,
user: selfUserId,
content: {
creator: selfUserId,
},
content: {},
}),
],
},
@@ -576,9 +731,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.create",
room: roomOne,
user: selfUserId,
content: {
creator: selfUserId,
},
content: {},
}),
],
},
@@ -610,9 +763,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.create",
room: roomTwo,
user: selfUserId,
content: {
creator: selfUserId,
},
content: {},
}),
],
},
@@ -757,7 +908,6 @@ describe("MatrixClient syncing", () => {
room: roomOne,
user: otherUserId,
content: {
creator: otherUserId,
room_version: "9",
},
});
@@ -843,7 +993,6 @@ describe("MatrixClient syncing", () => {
room: roomOne,
user: otherUserId,
content: {
creator: otherUserId,
room_version: testMeta.roomVersion,
},
});
@@ -1371,9 +1520,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.create",
room: roomOne,
user: selfUserId,
content: {
creator: selfUserId,
},
content: {},
}),
],
} as Partial<IJoinedRoom>,
@@ -1470,9 +1617,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.create",
room: roomOne,
user: selfUserId,
content: {
creator: selfUserId,
},
content: {},
}),
],
},
@@ -1586,6 +1731,66 @@ describe("MatrixClient syncing", () => {
});
});
});
it("should apply encrypted notification logic for events within the same sync blob", async () => {
const roomId = "!room123:server";
const syncData = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [],
},
timeline: {
events: [
utils.mkEvent({
room: roomId,
event: true,
skey: "",
type: EventType.RoomEncryption,
content: {},
}),
utils.mkMessage({
room: roomId,
user: otherUserId,
msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomId,
mship: "join",
user: otherUserId,
}),
utils.mkMembership({
room: roomId,
mship: "join",
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomId,
user: selfUserId,
content: {},
}),
],
},
},
},
},
} as unknown as ISyncResponse;
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
});
});
describe("of a room", () => {
@@ -1867,4 +2072,124 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
idbClient.stopClient();
idbHttpBackend.stop();
});
it("should query server for which thread a 2nd order relation belongs to and stash in sync accumulator", async () => {
const roomId = "!room:example.org";
async function startClient(client: MatrixClient): Promise<void> {
await Promise.all([
idbClient.startClient({
// Without this all events just go into the main timeline
threadSupport: true,
}),
idbHttpBackend.flushAllExpected(),
emitPromise(idbClient, ClientEvent.Room),
]);
}
function assertEventsExpected(client: MatrixClient): void {
const room = client.getRoom(roomId);
const mainTimelineEvents = room!.getLiveTimeline().getEvents();
expect(mainTimelineEvents).toHaveLength(1);
expect(mainTimelineEvents[0].getContent().body).toEqual("Test");
const thread = room!.getThread("$someThreadId")!;
expect(thread.replayEvents).toHaveLength(1);
expect(thread.replayEvents![0].getRelation()!.key).toEqual("🪿");
}
let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
store: new IndexedDBStore({
indexedDB: global.indexedDB,
dbName: "test",
}),
});
let idbHttpBackend = idbTestClient.httpBackend;
let idbClient = idbTestClient.client;
await idbClient.store.startup();
idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] });
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
const syncRoomSection = {
join: {
[roomId]: {
timeline: {
prev_batch: "foo",
events: [
mkMessage({
room: roomId,
user: selfUserId,
msg: "Test",
}),
mkEvent({
room: roomId,
user: selfUserId,
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: "$someUnknownEvent",
key: "🪿",
},
},
type: "m.reaction",
}),
],
},
},
},
};
idbHttpBackend.when("GET", "/sync").respond(200, {
...syncData,
rooms: syncRoomSection,
});
idbHttpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/event/%24someUnknownEvent`).respond(
200,
mkEvent({
room: roomId,
user: selfUserId,
content: {
"body": "Thread response",
"m.relates_to": {
rel_type: THREAD_RELATION_TYPE.name,
event_id: "$someThreadId",
},
},
type: "m.room.message",
}),
);
await startClient(idbClient);
assertEventsExpected(idbClient);
idbHttpBackend.verifyNoOutstandingExpectation();
// Force sync accumulator to persist, reset client, assert it doesn't re-fetch event on next start-up
await idbClient.store.save(true);
await idbClient.stopClient();
await idbClient.store.destroy();
await idbHttpBackend.stop();
idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
store: new IndexedDBStore({
indexedDB: global.indexedDB,
dbName: "test",
}),
});
idbHttpBackend = idbTestClient.httpBackend;
idbClient = idbTestClient.client;
await idbClient.store.startup();
idbHttpBackend.when("GET", "/versions").respond(200, { versions: ["v1.4"] });
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
idbHttpBackend.when("GET", "/sync").respond(200, syncData);
await startClient(idbClient);
assertEventsExpected(idbClient);
idbHttpBackend.verifyNoOutstandingExpectation();
await idbClient.stopClient();
await idbHttpBackend.stop();
});
});
@@ -89,7 +89,7 @@ describe("MatrixClient syncing", () => {
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!;
room.addLiveEvents([thread.rootEvent]);
await room.addLiveEvents([thread.rootEvent]);
// Initialize read receipt datastructure before testing the reaction
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
@@ -179,7 +179,6 @@ describe("MatrixClient syncing", () => {
events: [
{
content: {
creator: userB,
room_version: "9",
},
origin_server_ts: 1,
@@ -377,6 +376,7 @@ describe("MatrixClient syncing", () => {
},
[Category.Leave]: {},
[Category.Invite]: {},
[Category.Knock]: {},
},
};
}
-170
View File
@@ -1,170 +0,0 @@
/*
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: {
[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");
});
});
});
+85 -97
View File
@@ -20,7 +20,7 @@ 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 { IRoomEvent, IStateEvent } from "../../src";
import {
MatrixClient,
MatrixEvent,
@@ -39,9 +39,10 @@ import {
} from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncApiOptions, SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client";
import { IStoredClientOpts } from "../../src";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
@@ -120,7 +121,7 @@ describe("SlidingSyncSdk", () => {
await client!.initCrypto();
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
}
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
};
@@ -187,7 +188,7 @@ describe("SlidingSyncSdk", () => {
[roomA]: {
name: "A",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
@@ -202,7 +203,7 @@ describe("SlidingSyncSdk", () => {
name: "B",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
@@ -214,7 +215,7 @@ describe("SlidingSyncSdk", () => {
name: "C",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
@@ -227,7 +228,7 @@ describe("SlidingSyncSdk", () => {
name: "D",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
@@ -263,7 +264,7 @@ describe("SlidingSyncSdk", () => {
[roomF]: {
name: "#foo:localhost",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
@@ -279,7 +280,7 @@ describe("SlidingSyncSdk", () => {
name: "G",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
],
@@ -291,7 +292,7 @@ describe("SlidingSyncSdk", () => {
name: "H",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
@@ -301,67 +302,57 @@ describe("SlidingSyncSdk", () => {
},
};
it("can be created with required_state and timeline", () => {
it("can be created with required_state and timeline", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
await emitPromise(client!, ClientEvent.Room);
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);
expect(gotRoom).toBeTruthy();
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", () => {
it("can be created with timeline only", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
await emitPromise(client!, ClientEvent.Room);
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);
expect(gotRoom).toBeTruthy();
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", () => {
it("can be created with a highlight_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) {
return;
}
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
data[roomC].highlight_count,
);
});
it("can be created with a notification_count", () => {
it("can be created with a notification_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) {
return;
}
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
data[roomD].notification_count,
);
});
it("can be created with an invited/joined_count", () => {
it("can be created with an invited/joined_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
await emitPromise(client!, ClientEvent.Room);
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);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom!.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
});
it("can be created with live events", () => {
let seenLiveEvent = false;
it("can be created with live events", async () => {
const seenLiveEventDeferred = defer<boolean>();
const listener = (
ev: MatrixEvent,
room?: Room,
@@ -371,43 +362,37 @@ describe("SlidingSyncSdk", () => {
) => {
if (timelineData?.liveEvent) {
assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
seenLiveEvent = true;
seenLiveEventDeferred.resolve(true);
}
};
client!.on(RoomEvent.Timeline, listener);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]);
await emitPromise(client!, ClientEvent.Room);
client!.off(RoomEvent.Timeline, listener);
const gotRoom = client!.getRoom(roomH);
expect(gotRoom).toBeDefined();
if (gotRoom == null) {
return;
}
expect(gotRoom.name).toEqual(data[roomH].name);
expect(gotRoom.getMyMembership()).toEqual("join");
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomH].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
// check the entire timeline is correct
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline);
expect(seenLiveEvent).toBe(true);
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
});
it("can be created with invite_state", () => {
it("can be created with invite_state", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeDefined();
if (gotRoom == null) {
return;
}
expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getMyMembership()).toEqual("invite");
expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite);
});
it("uses the 'name' field to caluclate the room name", () => {
it("uses the 'name' field to caluclate the room name", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomF);
expect(gotRoom).toBeDefined();
if (gotRoom == null) {
return;
}
expect(gotRoom.name).toEqual(data[roomF].name);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomF].name);
});
describe("updating", () => {
@@ -419,33 +404,33 @@ describe("SlidingSyncSdk", () => {
name: data[roomA].name,
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
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();
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
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();
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Restricted);
});
it("can update with a new highlight_count", async () => {
@@ -456,11 +441,11 @@ describe("SlidingSyncSdk", () => {
highlight_count: 1,
});
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
});
it("can update with a new notification_count", async () => {
@@ -471,11 +456,11 @@ describe("SlidingSyncSdk", () => {
notification_count: 1,
});
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
});
it("can update with a new joined_count", () => {
@@ -486,11 +471,11 @@ describe("SlidingSyncSdk", () => {
joined_count: 1,
});
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
expect(gotRoom!.getJoinedMemberCount()).toEqual(1);
});
// Regression test for a bug which caused the timeline entries to be out-of-order
@@ -512,7 +497,7 @@ describe("SlidingSyncSdk", () => {
initial: true, // e.g requested via room subscription
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
@@ -530,7 +515,7 @@ describe("SlidingSyncSdk", () => {
);
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline);
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), oldTimeline);
});
});
});
@@ -617,7 +602,7 @@ describe("SlidingSyncSdk", () => {
name: "Room with Invite",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
@@ -626,9 +611,9 @@ describe("SlidingSyncSdk", () => {
await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name);
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room).toBeTruthy();
const inviteeMember = room.getMember(invitee)!;
expect(inviteeMember).toBeDefined();
expect(inviteeMember).toBeTruthy();
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
});
@@ -723,7 +708,7 @@ describe("SlidingSyncSdk", () => {
],
});
globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeDefined();
expect(globalData).toBeTruthy();
expect(globalData.getContent()).toEqual(globalContent);
});
@@ -733,7 +718,7 @@ describe("SlidingSyncSdk", () => {
name: "Room with account data",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
@@ -744,6 +729,7 @@ describe("SlidingSyncSdk", () => {
foo: "bar",
};
const roomType = "test";
await emitPromise(client!, ClientEvent.Room);
ext.onResponse({
rooms: {
[roomId]: [
@@ -755,9 +741,9 @@ describe("SlidingSyncSdk", () => {
},
});
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room).toBeTruthy();
const event = room.getAccountData(roomType)!;
expect(event).toBeDefined();
expect(event).toBeTruthy();
expect(event.getContent()).toEqual(roomContent);
});
@@ -936,15 +922,16 @@ describe("SlidingSyncSdk", () => {
name: "Room with typing",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
initial: true,
});
await emitPromise(client!, ClientEvent.Room);
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room).toBeTruthy();
expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({
rooms: {
@@ -976,7 +963,7 @@ describe("SlidingSyncSdk", () => {
name: "Room with typing",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
@@ -984,7 +971,7 @@ describe("SlidingSyncSdk", () => {
initial: true,
});
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room).toBeTruthy();
expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({
rooms: {
@@ -1062,7 +1049,7 @@ describe("SlidingSyncSdk", () => {
name: "Room with receipts",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
{
@@ -1077,12 +1064,13 @@ describe("SlidingSyncSdk", () => {
],
initial: true,
});
await emitPromise(client!, ClientEvent.Room);
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room).toBeTruthy();
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567));
const receipt = room.getReadReceiptForUserId(alice);
expect(receipt).toBeDefined();
expect(receipt).toBeTruthy();
expect(receipt?.eventId).toEqual(lastEvent.event_id);
expect(receipt?.data.ts).toEqual(1234567);
expect(receipt?.data.thread_id).toBeFalsy();
+12 -11
View File
@@ -1161,11 +1161,6 @@ describe("SlidingSync", () => {
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
await httpBackend!.flushAllExpected();
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail.
expect(failPromise).rejects.toEqual(gotTxnIds[0]);
expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
const okPromise = slidingSync.setListRanges("a", [[0, 20]]);
let txnId: string | undefined;
httpBackend!
@@ -1180,8 +1175,12 @@ describe("SlidingSync", () => {
txn_id: txnId,
};
});
await httpBackend!.flushAllExpected();
await okPromise;
await Promise.all([
expect(failPromise).rejects.toEqual(gotTxnIds[0]),
expect(failPromise2).rejects.toEqual(gotTxnIds[1]),
httpBackend!.flushAllExpected(),
okPromise,
]);
expect(txnId).toBeDefined();
});
@@ -1200,7 +1199,6 @@ describe("SlidingSync", () => {
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail.
expect(A).rejects.toEqual(gotTxnIds[0]);
const C = slidingSync.setListRanges("a", [[0, 20]]);
let pendingC = true;
@@ -1217,9 +1215,12 @@ describe("SlidingSync", () => {
txn_id: gotTxnIds[1],
};
});
await httpBackend!.flushAllExpected();
// A is rejected, see above
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
await Promise.all([
expect(A).rejects.toEqual(gotTxnIds[0]),
httpBackend!.flushAllExpected(),
// A is rejected, see above
expect(B).resolves.toEqual(gotTxnIds[1]), // B is resolved
]);
expect(pendingC).toBe(true); // C is pending still
});
it("should do nothing for unknown txn_ids", async () => {
+1 -1
View File
@@ -23,5 +23,5 @@ try {
global.Olm = require("@matrix-org/olm");
logger.log("loaded libolm");
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available");
logger.warn("unable to run crypto tests: libolm not available", e);
}
+7 -2
View File
@@ -75,8 +75,6 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
const listener = (url: string, options: RequestInit) =>
this.onKeyUploadRequest(resolveOneTimeKeys, options);
// catch both r0 and v3 variants
fetchMock.post(new URL("/_matrix/client/r0/keys/upload", homeserverUrl).toString(), listener);
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
});
}
@@ -145,6 +143,13 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
return this.deviceKeys.keys[keyIds[0]];
}
/**
* If the device keys have already been uploaded, return them. Else return null.
*/
public getUploadedDeviceKeys(): IDeviceKeys | null {
return this.deviceKeys;
}
/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
+119
View File
@@ -0,0 +1,119 @@
/*
Copyright 2023 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 fetchMock from "fetch-mock-jest";
import { MapWithDefault } from "../../src/utils";
import { IDownloadKeyResult } from "../../src";
import { IDeviceKeys } from "../../src/@types/crypto";
import { E2EKeyReceiver } from "./E2EKeyReceiver";
/**
* An object which intercepts `/keys/query` fetches via fetch-mock.
*/
export class E2EKeyResponder {
private deviceKeysByUserByDevice = new MapWithDefault<string, Map<string, any>>(() => new Map());
private e2eKeyReceiversByUser = new Map<string, E2EKeyReceiver>();
private masterKeysByUser: Record<string, any> = {};
private selfSigningKeysByUser: Record<string, any> = {};
private userSigningKeysByUser: Record<string, any> = {};
/**
* Construct a new E2EKeyResponder.
*
* It will immediately register an intercept of `/keys/query` requests for the given homeserverUrl.
* Only /query requests made to this server will be intercepted: this allows a single test to use more than one
* client and have the keys collected separately.
*
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
// set up a listener for /keys/query.
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
}
private onKeyQueryRequest(options: RequestInit) {
const content = JSON.parse(options.body as string);
const usersToReturn = Object.keys(content["device_keys"]);
const response = {
device_keys: {} as { [userId: string]: any },
master_keys: {} as { [userId: string]: any },
self_signing_keys: {} as { [userId: string]: any },
user_signing_keys: {} as { [userId: string]: any },
failures: {} as { [serverName: string]: any },
};
for (const user of usersToReturn) {
const userKeys = this.deviceKeysByUserByDevice.get(user);
if (userKeys !== undefined) {
response.device_keys[user] = Object.fromEntries(userKeys.entries());
}
const e2eKeyReceiver = this.e2eKeyReceiversByUser.get(user);
if (e2eKeyReceiver !== undefined) {
const deviceKeys = e2eKeyReceiver.getUploadedDeviceKeys();
if (deviceKeys !== null) {
response.device_keys[user] ??= {};
response.device_keys[user][deviceKeys.device_id] = deviceKeys;
}
}
if (this.masterKeysByUser.hasOwnProperty(user)) {
response.master_keys[user] = this.masterKeysByUser[user];
}
if (this.selfSigningKeysByUser.hasOwnProperty(user)) {
response.self_signing_keys[user] = this.selfSigningKeysByUser[user];
}
if (this.userSigningKeysByUser.hasOwnProperty(user)) {
response.user_signing_keys[user] = this.userSigningKeysByUser[user];
}
}
return response;
}
/**
* Add a set of device keys for return by a future `/keys/query`, as if they had been `/upload`ed
*
* @param keys - device keys for this device.
*/
public addDeviceKeys(keys: IDeviceKeys) {
this.deviceKeysByUserByDevice.getOrCreate(keys.user_id).set(keys.device_id, keys);
}
/** Add a set of cross-signing keys for return by a future `/keys/query`, as if they had been `/keys/device_signing/upload`ed
*
* @param data cross-signing data
*/
public addCrossSigningData(
data: Pick<IDownloadKeyResult, "master_keys" | "self_signing_keys" | "user_signing_keys">,
) {
Object.assign(this.masterKeysByUser, data.master_keys);
Object.assign(this.selfSigningKeysByUser, data.self_signing_keys);
Object.assign(this.userSigningKeysByUser, data.user_signing_keys);
}
/**
* Add an E2EKeyReceiver to poll for uploaded keys
*
* Any keys which have been uploaded to the given `E2EKeyReceiver` at the time of the `/keys/query` request will
* be added to the response.
*
* @param e2eKeyReceiver
*/
public addKeyReceiver(userId: string, e2eKeyReceiver: E2EKeyReceiver) {
this.e2eKeyReceiversByUser.set(userId, e2eKeyReceiver);
}
}
+1 -1
View File
@@ -75,7 +75,7 @@ export class SyncResponder implements ISyncResponder {
*/
public constructor(homeserverUrl: string) {
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
fetchMock.get("begin:" + new URL("/_matrix/client/r0/sync?", homeserverUrl).toString(), (_url, _options) =>
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
this.onSyncRequest(),
);
}
-1
View File
@@ -86,7 +86,6 @@ export const mockClientMethodsEvents = () => ({
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
doesServerSupportSeparateAddAndBind: jest.fn(),
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCapabilities: jest.fn().mockReturnValue({}),
+92
View File
@@ -0,0 +1,92 @@
/*
Copyright 2023 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 fetchMock from "fetch-mock-jest";
import { KeyBackupInfo } from "../../src/crypto-api";
/**
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
*
* @param homeserverUrl - the homeserver url for the client under test
*/
export function mockInitialApiRequests(homeserverUrl: string) {
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
fetchMock.postOnce(new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
filter_id: "fid",
});
}
/**
* Mock the requests needed to set up cross signing
*
* Return 404 error for `GET _matrix/client/v3/user/:userId/account_data/:type` request
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
*/
export function mockSetupCrossSigningRequests(): void {
// have account_data requests return an empty object
fetchMock.get("express:/_matrix/client/v3/user/:userId/account_data/:type", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
});
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// ... and one to upload the cross-signing keys (with UIA)
fetchMock.post(
// legacy crypto uses /unstable/; /v3/ is correct
{
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
name: "upload-keys",
},
{},
);
}
/**
* Mock out requests to `/room_keys/version`.
*
* Returns `404 M_NOT_FOUND` for GET requests until `POST room_keys/version` is called.
* Once the POST is done, `GET /room_keys/version` will return the posted backup
* instead of 404.
*
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
*/
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No current backup version",
},
});
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
overwriteRoutes: true,
});
return {
version: backupVersion,
};
});
}
+53
View File
@@ -0,0 +1,53 @@
/*
Copyright 2023 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 { OidcClientConfig } from "../../src";
import { ValidatedIssuerMetadata } from "../../src/oidc/validate";
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);
return {
issuer,
account: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
});
+1
View File
@@ -0,0 +1 @@
/env
+654
View File
@@ -0,0 +1,654 @@
#!/bin/env python
#
# Copyright 2023 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 is a Python script to generate test data for crypto tests.
To run it:
python -m venv env
./env/bin/pip install cryptography canonicaljson
./env/bin/python generate-test-data.py > index.ts
"""
import base64
import json
import base58
from canonicaljson import encode_canonical_json
from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives import hashes, padding, hmac
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from random import randbytes, seed
ALICE_DATA = {
"TEST_USER_ID": "@alice:localhost",
"TEST_DEVICE_ID": "test_device",
"TEST_ROOM_ID": "!room:id",
# any 32-byte string can be an ed25519 private key.
"TEST_DEVICE_PRIVATE_KEY_BYTES": b"deadbeefdeadbeefdeadbeefdeadbeef",
# any 32-byte string can be an curve25519 private key.
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"deadmuledeadmuledeadmuledeadmule",
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"doyouspeakwhaaaaaaaaaaaaaaaaaale",
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"useruseruseruseruseruseruseruser",
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"selfselfselfselfselfselfselfself",
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
"B64_BACKUP_DECRYPTION_KEY": "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
}
BOB_DATA = {
"TEST_USER_ID": "@bob:xyz",
"TEST_DEVICE_ID": "bob_device",
"TEST_ROOM_ID": "!room:id",
# any 32-byte string can be an ed25519 private key.
"TEST_DEVICE_PRIVATE_KEY_BYTES": b"Deadbeefdeadbeefdeadbeefdeadbeef",
# any 32-byte string can be an curve25519 private key.
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"Deadmuledeadmuledeadmuledeadmule",
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
"B64_BACKUP_DECRYPTION_KEY": "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
}
def main() -> None:
print(
f"""\
/* Test data for cryptography tests
*
* Do not edit by hand! This file is generated by `./generate-test-data.py`
*/
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
import {{ IDownloadKeyResult, IEvent }} from "../../../src";
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
// Alice data
{build_test_data(ALICE_DATA)}
// Bob data
{build_test_data(BOB_DATA, "BOB_")}
""",
end="",
)
# Use static seed to have stable random test data upon new generation
seed(10)
def build_test_data(user_data, prefix = "") -> str:
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["TEST_DEVICE_PRIVATE_KEY_BYTES"]
)
device_curve_key = x25519.X25519PrivateKey.from_private_bytes(
user_data["TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES"]
)
b64_public_key = encode_base64(
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
device_data = {
"algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
"device_id": user_data["TEST_DEVICE_ID"],
"keys": {
f"curve25519:{user_data['TEST_DEVICE_ID']}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
f"ed25519:{user_data['TEST_DEVICE_ID']}": b64_public_key,
},
"signatures": {user_data['TEST_USER_ID']: {}},
"user_id": user_data["TEST_USER_ID"],
}
device_data["signatures"][user_data["TEST_USER_ID"]][f"ed25519:{user_data['TEST_DEVICE_ID']}"] = sign_json(
device_data, private_key
)
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
b64_master_private_key = encode_base64(user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_self_signing_public_key = encode_base64(
self_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw
)
)
b64_self_signing_private_key = encode_base64( user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_user_signing_public_key = encode_base64(
user_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw
)
)
b64_user_signing_private_key = encode_base64(user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
backup_decryption_key = x25519.X25519PrivateKey.from_private_bytes(
base64.b64decode(user_data["B64_BACKUP_DECRYPTION_KEY"])
)
b64_backup_public_key = encode_base64(
backup_decryption_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
backup_data = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": b64_backup_public_key,
},
}
# sign with our device key
sig = sign_json(backup_data["auth_data"], private_key)
backup_data["auth_data"]["signatures"] = {
user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": sig}
}
set_of_exported_room_keys = [build_exported_megolm_key(device_curve_key)[0], build_exported_megolm_key(device_curve_key)[0]]
additional_exported_room_key, additional_exported_ed_key = build_exported_megolm_key(device_curve_key)
ratcheted_exported_room_key = symetric_ratchet_step_of_megolm_key(additional_exported_room_key, additional_exported_ed_key)
otk_to_sign = {
"key": user_data['OTK']
}
# sign our public otk key with our device key
otk = sign_json(otk_to_sign, private_key)
otks = {
user_data["TEST_USER_ID"]: {
user_data['TEST_DEVICE_ID']: {
"signed_curve25519:AAAAHQ": {
"key": user_data["OTK"],
"signatures": {
user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": otk}
}
}
}
}
}
backed_up_room_key = encrypt_megolm_key_for_backup(additional_exported_room_key, backup_decryption_key.public_key())
clear_event, encrypted_event = generate_encrypted_event_content(additional_exported_room_key, additional_exported_ed_key, device_curve_key)
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
return f"""\
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
/** The base64-encoded public ed25519 key for this device */
export const {prefix}TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
/** Signed device data, suitable for returning from a `/keys/query` call */
export const {prefix}SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
/** base64-encoded public master cross-signing key */
export const {prefix}MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
/** base64-encoded private master cross-signing key */
export const {prefix}MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}";
/** base64-encoded public self cross-signing key */
export const {prefix}SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}";
/** base64-encoded private self signing cross-signing key */
export const {prefix}SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}";
/** base64-encoded public user cross-signing key */
export const {prefix}USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}";
/** base64-encoded private user signing cross-signing key */
export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
};
/** Signed OTKs, returned by `POST /keys/claim` */
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
/** base64-encoded backup decryption (private) key */
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
/** Backup decryption key in export format */
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
json.dumps(set_of_exported_room_keys, indent=4)
};
/** An exported megolm session */
export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = {
json.dumps(additional_exported_room_key, indent=4)
};
/** A ratcheted version of {prefix}MEGOLM_SESSION_DATA */
export const {prefix}RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = {
json.dumps(ratcheted_exported_room_key, indent=4)
};
/** The key from {prefix}MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
export const {prefix}CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {json.dumps(backed_up_room_key, indent=4)};
/** A test clear event */
export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, indent=4)};
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
"""
def build_cross_signing_keys_data(user_data) -> dict:
"""Build the signed cross-signing-keys data for return from /keys/query"""
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_self_signing_public_key = encode_base64(
self_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw
)
)
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_user_signing_public_key = encode_base64(
user_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw
)
)
# create without signatures initially
cross_signing_keys_data = {
"master_keys": {
user_data["TEST_USER_ID"]: {
"keys": {
f"ed25519:{b64_master_public_key}": b64_master_public_key,
},
"user_id": user_data["TEST_USER_ID"],
"usage": ["master"],
}
},
"self_signing_keys": {
user_data["TEST_USER_ID"]: {
"keys": {
f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key,
},
"user_id": user_data["TEST_USER_ID"],
"usage": ["self_signing"],
},
},
"user_signing_keys": {
user_data["TEST_USER_ID"]: {
"keys": {
f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key,
},
"user_id": user_data["TEST_USER_ID"],
"usage": ["user_signing"],
},
},
}
# sign the sub-keys with the master
for k in ["self_signing_keys", "user_signing_keys"]:
to_sign = cross_signing_keys_data[k][user_data["TEST_USER_ID"]]
sig = sign_json(to_sign, master_private_key)
to_sign["signatures"] = {
user_data["TEST_USER_ID"]: {f"ed25519:{b64_master_public_key}": sig}
}
return cross_signing_keys_data
def encode_base64(input_bytes: bytes) -> str:
"""Encode with unpadded base64"""
output_bytes = base64.b64encode(input_bytes)
output_string = output_bytes.decode("ascii")
return output_string.rstrip("=")
def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
"""
Sign the given json object
Returns the base64-encoded signature of signing `input` following the Matrix
JSON signature algorithm [1]
[1]: https://spec.matrix.org/v1.7/appendices/#signing-details
"""
signatures = json_object.pop("signatures", {})
unsigned = json_object.pop("unsigned", None)
signature = private_key.sign(encode_canonical_json(json_object))
signature_base64 = encode_base64(signature)
json_object["signatures"] = signatures
if unsigned is not None:
json_object["unsigned"] = unsigned
return signature_base64
def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tuple[dict, ed25519.Ed25519PrivateKey]:
"""
Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format
that can be imported via importRoomKeys API.
Returns the exported key, the matching privat edKey (needed to encrypt)
"""
index = 0
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32))
# Just use radom bytes for the ratchet parts
ratchet = randbytes(32 * 4)
# exported key, start with version byte
exported_key = bytearray(b'\x01')
exported_key += index.to_bytes(4, 'big')
exported_key += ratchet
# KPub
exported_key += private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
megolm_export = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": encode_base64(
device_curve_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
),
"session_id": encode_base64(
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
),
"session_key": encode_base64(exported_key),
"sender_claimed_keys": {
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
},
"forwarding_curve25519_key_chain": [],
}
return megolm_export, private_key
def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed25519.Ed25519PrivateKey) -> dict:
"""
Very simple ratchet step from 0 to 1
Used to generate a ratcheted key to test unknown message index.
"""
session_key: str = previous["session_key"]
# Get the megolm R0 from the export format
decoded = base64.b64decode(session_key.encode("ascii"))
ri = decoded[5:133]
ri0 = ri[0:32]
ri1 = ri[32:64]
ri2 = ri[64:96]
ri3 = ri[96:128]
h = hmac.HMAC(ri3, hashes.SHA256())
h.update(b'x\03')
ri1_3 = h.finalize()
index = 1
private_key = megolm_private_key
# exported key, start with version byte
exported_key = bytearray(b'\x01')
exported_key += index.to_bytes(4, 'big')
exported_key += ri0
exported_key += ri1
exported_key += ri2
exported_key += ri1_3
# KPub
exported_key += private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
megolm_export = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": previous["sender_key"],
"session_id": previous["session_id"],
"session_key": encode_base64(exported_key),
"sender_claimed_keys": previous["sender_claimed_keys"],
"forwarding_curve25519_key_chain": [],
}
return megolm_export
def encrypt_megolm_key_for_backup(session_data: dict, backup_public_key: x25519.X25519PublicKey) -> dict:
"""
Encrypts an exported megolm key for key backup, using the m.megolm_backup.v1.curve25519-aes-sha2 algorithm.
"""
data = encode_canonical_json(session_data)
# Generate an ephemeral curve25519 key, and perform an ECDH with the ephemeral key
# and the backups public key to generate a shared secret.
# The public half of the ephemeral key, encoded using unpadded base64,
# becomes the ephemeral property of the session_data.
ephemeral_keypair = x25519.X25519PrivateKey.from_private_bytes(randbytes(32))
shared_secret = ephemeral_keypair.exchange(backup_public_key)
ephemeral = encode_base64(ephemeral_keypair.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw))
# Using the shared secret, generate 80 bytes by performing an HKDF using SHA-256 as the hash,
# with a salt of 32 bytes of 0, and with the empty string as the info.
# The first 32 bytes are used as the AES key, the next 32 bytes are used as the MAC key,
# and the last 16 bytes are used as the AES initialization vector.
salt = bytes(32)
info = b""
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=80,
salt=salt,
info=info,
)
raw_key = hkdf.derive(shared_secret)
aes_key = raw_key[:32]
mac = raw_key[32:64]
iv = raw_key[64:80]
# Stringify the JSON object, and encrypt it using AES-CBC-256 with PKCS#7 padding.
# This encrypted data, encoded using unpadded base64, becomes the ciphertext property of the session_data.
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv))
encryptor = cipher.encryptor()
padder = padding.PKCS7(128).padder()
padded_data = padder.update(data) + padder.finalize()
ct = encryptor.update(padded_data) + encryptor.finalize()
cipher_text = encode_base64(ct)
# Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above.
# The first 8 bytes of the resulting MAC are base64-encoded, and become the mac property of the session_data.
h = hmac.HMAC(mac, hashes.SHA256())
# h.update(ct)
signature = h.finalize()
mac = encode_base64(signature[:8])
encrypted_key = {
"first_message_index": 1,
"forwarded_count": 0,
"is_verified": False,
"session_data": {
"ciphertext": cipher_text,
"ephemeral": ephemeral,
"mac": mac
}
}
return encrypted_key
def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519PrivateKey, curve_key: x25519.X25519PrivateKey) -> tuple[dict, dict]:
"""
Encrypts an event using the given key in session export format.
Will not do any ratcheting, just encrypt at index 0.
"""
clear_event = {
"type": "m.room.message",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"msgtype": "m.text",
"body": "Hello world"
}
}
session_key: str = exported_key["session_key"]
# Get the megolm R0 from the export format
decoded = base64.b64decode(session_key.encode("ascii"))
r0 = decoded[5:133]
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=80,
salt=bytes(32),
info=b"MEGOLM_KEYS",
)
raw_key = hkdf.derive(r0)
aes_key = raw_key[:32]
mac = raw_key[32:64]
aes_iv = raw_key[64:80]
payload_json = {
"room_id": clear_event["room_id"],
"type": clear_event["type"],
"content": clear_event["content"]
}
payload_string = encode_canonical_json(payload_json)
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(aes_iv))
encryptor = cipher.encryptor()
padder = padding.PKCS7(128).padder()
padded_data = padder.update(payload_string)
padded_data += padder.finalize()
ct = encryptor.update(padded_data) + encryptor.finalize()
# The ratchet index i, and the cipher-text, are then packed
# into a message as described in Message format. Then the entire message
# (including the version bytes and all payload bytes) are passed through
# HMAC-SHA-256. The first 8 bytes of the MAC are appended to the message.
message = bytearray()
message += b'\x03'
# int tag for index
message += b'\x08'
# index is 0
message += b'\x00'
message += b'\x12'
# probably works only for short messages
message += len(ct).to_bytes(1, 'big')
# encrypted data
message += ct
h = hmac.HMAC(mac, hashes.SHA256())
h.update(message)
signature = h.finalize()
mac = signature[:8]
message += mac
# Finally, the authenticated message is signed using the Ed25519 keypair;
# the 64 byte signature is appended to the message
signature = ed_key.sign(bytes(message))
message += signature
cipher_text = encode_base64(message)
encrypted_payload = {
"algorithm" : "m.megolm.v1.aes-sha2",
"sender_key" : encode_base64(curve_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
"ciphertext" : cipher_text,
"session_id" : exported_key["session_id"],
"device_id" : "TEST_DEVICE"
}
encrypted_event = {
"type": "m.room.encrypted",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": encrypted_payload,
"event_id": "$event1",
"origin_server_ts": 1507753886000,
}
return clear_event, encrypted_event
def export_recovery_key(key_b64: str) -> str:
"""
Export a private recovery key as a recovery key that can be presented to users.
As per spec https://spec.matrix.org/v1.8/client-server-api/#recovery-key
"""
private_key_bytes = base64.b64decode(key_b64)
# The 256-bit curve25519 private key is prepended by the bytes 0x8B and 0x01
export_bytes = bytearray()
export_bytes += b'\x8b'
export_bytes += b'\x01'
export_bytes += private_key_bytes
# All the bytes in the string above, including the two header bytes,
# are XORed together to form a parity byte. This parity byte is appended to the byte string.
parity_byte = 0 #b'\x8b' ^ b'\x01'
[parity_byte := parity_byte ^ x for x in export_bytes]
export_bytes += parity_byte.to_bytes(1, 'big')
# The byte string is encoded using base58
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
return ' '.join(split)
if __name__ == "__main__":
main()
+451
View File
@@ -0,0 +1,451 @@
/* Test data for cryptography tests
*
* Do not edit by hand! This file is generated by `./generate-test-data.py`
*/
import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
import { IDownloadKeyResult, IEvent } from "../../../src";
import { KeyBackupSession, KeyBackupInfo } from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
// Alice data
export const TEST_USER_ID = "@alice:localhost";
export const TEST_DEVICE_ID = "test_device";
export const TEST_ROOM_ID = "!room:id";
/** The base64-encoded public ed25519 key for this device */
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU";
/** Signed device data, suitable for returning from a `/keys/query` call */
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "test_device",
"keys": {
"curve25519:test_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
"ed25519:test_device": "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU"
},
"user_id": "@alice:localhost",
"signatures": {
"@alice:localhost": {
"ed25519:test_device": "LmQC/yAUZJmkxZ+3L0nEwvtVWOzjqQqADWBhk+C47SPaFYHeV+E291mgXaSCJVeGltX+HC49Aw7nb6ga7sw0Aw"
}
}
};
/** base64-encoded public master cross-signing key */
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY";
/** base64-encoded private master cross-signing key */
export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "ZG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
/** base64-encoded public self cross-signing key */
export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY";
/** base64-encoded private self signing cross-signing key */
export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "c2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
/** base64-encoded public user cross-signing key */
export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY";
/** base64-encoded private user signing cross-signing key */
export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "dXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
"@alice:localhost": {
"keys": {
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY"
},
"user_id": "@alice:localhost",
"usage": [
"master"
]
}
},
"self_signing_keys": {
"@alice:localhost": {
"keys": {
"ed25519:aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY": "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY"
},
"user_id": "@alice:localhost",
"usage": [
"self_signing"
],
"signatures": {
"@alice:localhost": {
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "XfhYEhZmOs8BJdb3viatILBZ/bElsHXEW28V4tIaY5CxrBR0YOym3yZHWmRmypXessHZAKOhZn3yBMXzdajyCw"
}
}
}
},
"user_signing_keys": {
"@alice:localhost": {
"keys": {
"ed25519:g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY": "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY"
},
"user_id": "@alice:localhost",
"usage": [
"user_signing"
],
"signatures": {
"@alice:localhost": {
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "6AkD1XM2H0/ebgP9oBdMKNeft7uxsrb0XN1CsjjHgeZCvCTMmv3BHlLiT/Hzy4fe8H+S1tr484dcXN/PIdnfDA"
}
}
}
}
};
/** Signed OTKs, returned by `POST /keys/claim` */
export const ONE_TIME_KEYS = {
"@alice:localhost": {
"test_device": {
"signed_curve25519:AAAAHQ": {
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
"signatures": {
"@alice:localhost": {
"ed25519:test_device": "25djC6Rk6gIgFBMVawY9X9LnY8XMMziey6lKqL8Q5Bbp7T1vw9uk0RE7eKO2a/jNLcYroO2xRztGhBrKz5sOCQ"
}
}
}
}
}
};
/** base64-encoded backup decryption (private) key */
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** Backup decryption key in export format */
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
"signatures": {
"@alice:localhost": {
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
}
}
}
};
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"session_id": "FYOoKQSwe4d9jhTZ/LQCZFJINjPEqZ7Or4Z08reP92M",
"session_key": "AQAAAABZ0jXQOprFfXe41tIFmAtHxflJp4O2hM/vzQQpOazOCFeWSoW5P3Z9Q+voU3eXehMwyP8/hm/Q8xLP6/PmJdy+71se/17kdFwcDGgLxBWfa4ODM9zlI4EjKbNqmiii5loJ7rBhA/XXaw80m0hfU6zTDX/KrO55J0Pt4vJ0LDa3LBWDqCkEsHuHfY4U2fy0AmRSSDYzxKmezq+GdPK3j/dj",
"sender_claimed_keys": {
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
},
"forwarding_curve25519_key_chain": []
},
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"session_id": "mPYSGA2l1tOQiipEDEVYhDSdTSFh2lDW1qpGKYZRxTc",
"session_key": "AQAAAAAHwgkB49BTPAEGTCK6degxUIbl8GPG2ugPRYhNtOpNic63u11+baXFfjDw5fmVfD1gJXpQQjGsqrIYioxrB1xzl7mfb942UHhYdaMQZowpp1fSpJVsxR5TddUU2EWifYD9EQsoz8mY1zqoazm4vUP4v9yxaTcUBj2c6HMJCY0gCJj2EhgNpdbTkIoqRAxFWIQ0nU0hYdpQ1taqRimGUcU3",
"sender_claimed_keys": {
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
},
"forwarding_curve25519_key_chain": []
}
];
/** An exported megolm session */
export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
"session_key": "AQAAAABXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIXBlK8tAfJo3cJnlh7F4ltEOAqrdME6dU0zXTkqXmURqYqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K",
"sender_claimed_keys": {
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
},
"forwarding_curve25519_key_chain": []
};
/** A ratcheted version of MEGOLM_SESSION_DATA */
export const RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
"session_key": "AQAAAAFXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIUWvpwC7by/yg231+gyzu9lDHAU4ivCj48pt7WGiORWmIqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K",
"sender_claimed_keys": {
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
},
"forwarding_curve25519_key_chain": []
};
/** The key from MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"first_message_index": 1,
"forwarded_count": 0,
"is_verified": false,
"session_data": {
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//Kzlq2o5CssPsw+88gRehkAsPg9Zp5G9sL9to6giltvTWTbsaQpmvv3HLmBOYSFIxvyZrOT/Ffqu325f0IEsKcyV2BdIkw8Ob9Xt+VWoe4MYEGG6y1T8W125zeFgKWI4Ow76uput64H9zZjIo+Cc+hCTO9Ea4EnosSjizCotevkNck7C/zGgfhBikiohROb6SbaZgxicSsEDZ+f7brnri9yP3iXS3PMDHHpa1+XzG2VOG/Y9OQZpkPq+pbLrCC+NWJeJPslDAK5i+RURwzjnPmaHKCRHTq86CwhFyiCDf61MGwCY3xjrmBJg44BCdxWqCx0YJvwsvVqqnl4vTieUfrwThNPsQ81aVkDHvlmrgrTt8icDa8jTJhu34jem+pbRSEM5aJikV4B+zYiLz+dH/v6UpYA2eG8ReOvwpPXp6CAcIlplRPpWbMBeLFVcPkT4KAXTp9exFpB4on4pf8OsaDomlt4qAA0rhAZmhPWPKcU/A0Tz4gyMu54OivVtw1SPj+5Iq+YDQ8jB6Po3ApzMf6fwF9x/FjevbboFB05X2Jr0NrbFqXMOUwXHMgDAGiIWX8+gkmmbaiNWqg2etjN94pobQSGZelb18XGN7kuwMk+Zwk7A",
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
"mac": "OibmACbORhI"
}
};
/** A test clear event */
export const CLEAR_EVENT: Partial<IEvent> = {
"type": "m.room.message",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"msgtype": "m.text",
"body": "Hello world"
}
};
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
export const ENCRYPTED_EVENT: Partial<IEvent> = {
"type": "m.room.encrypted",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"ciphertext": "AwgAEnAkBmciEAyhh1j6DCk29UXJ7kv/kvayUNfuNT0iAioLxcXjFXOZ5ho3jF1/wrytlt0Lb298uMM67OxdVMi+/mMfYpwlvi07P9cIH6CMSj8tyhYoWl0SrKY6tkPf5GWOlRSRRKbziXa96FHXvnA3V2FCAIGtAe3G4ei5RPbhkmKAFBLAen33/D6MjJVqU8Ojr5vTkgls5eyirarlVpsmnH06alDaxO8avrU0NL+Vsw26xvlUQgEMOnUJ",
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
"device_id": "TEST_DEVICE"
},
"event_id": "$event1",
"origin_server_ts": 1507753886000
};
// Bob data
export const BOB_TEST_USER_ID = "@bob:xyz";
export const BOB_TEST_DEVICE_ID = "bob_device";
export const BOB_TEST_ROOM_ID = "!room:id";
/** The base64-encoded public ed25519 key for this device */
export const BOB_TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50";
/** Signed device data, suitable for returning from a `/keys/query` call */
export const BOB_SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "bob_device",
"keys": {
"curve25519:bob_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
"ed25519:bob_device": "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50"
},
"user_id": "@bob:xyz",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "4ApBs9jaeGyfdYaWRUdBvQAkDyXjACJ9KJ0xLHMgiFT/1yo6VqPTx2iziKGnrBiGhbtKNxEhDPOvZZkBU73cDQ"
}
}
};
/** base64-encoded public master cross-signing key */
export const BOB_MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA";
/** base64-encoded private master cross-signing key */
export const BOB_MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "RG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
/** base64-encoded public self cross-signing key */
export const BOB_SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A";
/** base64-encoded private self signing cross-signing key */
export const BOB_SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "U2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
/** base64-encoded public user cross-signing key */
export const BOB_USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw";
/** base64-encoded private user signing cross-signing key */
export const BOB_USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "VXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const BOB_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
"@bob:xyz": {
"keys": {
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA"
},
"user_id": "@bob:xyz",
"usage": [
"master"
]
}
},
"self_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
},
"user_id": "@bob:xyz",
"usage": [
"self_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "RxM8iJU6ZkyzQSVtNnXIJMPyEahVsN+fQQTBNKAs+kqySFyXBgchx+8czZaAhJCpXh9gD1nskT4yyFd2eyUXBw"
}
}
}
},
"user_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
},
"user_id": "@bob:xyz",
"usage": [
"user_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "jF8fvnPZulrPyh/4E8dNDVBP3iHHl9bRc+rRArVyGzoom+uVrokOck7BN2YmPyCRFZJJx7fgRA1Bveyu+mTVAg"
}
}
}
}
};
/** Signed OTKs, returned by `POST /keys/claim` */
export const BOB_ONE_TIME_KEYS = {
"@bob:xyz": {
"bob_device": {
"signed_curve25519:AAAAHQ": {
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg"
}
}
}
}
}
};
/** base64-encoded backup decryption (private) key */
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** Backup decryption key in export format */
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
}
}
}
};
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0",
"session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
"sender_claimed_keys": {
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
},
"forwarding_curve25519_key_chain": []
},
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "+07YOpSgdZ1X9le3n3NMByw0V1B0H0Djnbm76jgmWoo",
"session_key": "AQAAAAAjWfIMo9+BWS8IvhfsQuomxXXXGy11tJs0ej505xxd1RzOIP4ftq3MbZYsfH8kqSMBc2l1Ym2u3Dksv2/nR0zGQeNIgOxeMuwHU3Ry7+DdV1I96blPylVCCn/f5RAy6smKoaeylptPdXgVXmw3YBBUVYpHpm+xCIUUp9foAdb8hftO2DqUoHWdV/ZXt59zTAcsNFdQdB9A4525u+o4JlqK",
"sender_claimed_keys": {
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
},
"forwarding_curve25519_key_chain": []
}
];
/** An exported megolm session */
export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
"session_key": "AQAAAADZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9XoinBNIWr/gkyepuAKiQqemlc8J5amD9OkmbVkmnrxP1uyYMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX",
"sender_claimed_keys": {
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
},
"forwarding_curve25519_key_chain": []
};
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
export const BOB_RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
"session_key": "AQAAAAHZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9Xoil2JdGx9oPqR0dFVh661Aqs86rJRbQ4IeRiuEm35VMxboMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX",
"sender_claimed_keys": {
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
},
"forwarding_curve25519_key_chain": []
};
/** The key from BOB_MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"first_message_index": 1,
"forwarded_count": 0,
"is_verified": false,
"session_data": {
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3IcGylnoUcS3doVkJZiprXytXMP89AKcgv5Dj7mS2ZdvNGE+Atro74bzZ5yot5BrE0ZE5SjoUBPLaLMMu9HopLIV+qx01Rc3F0wmkocSPo51N0nv6wvO5Cst0FiOGHDK6r1pFlgDEJLmBkOyC4e8oMVbKTJzsSQVbJ8tJ37xuhI+T5P0ZlmiqKDqYRp8uh50w+txLEixYhEUunFgCTt1DAmiS9pLNYhLyl1ggwuQjzZe+AV6timbRxNJy18/AEcPomJw7z/pxYIiNLHRKOC13Wp8kGWx9cOgfMQ5KmBuLS8psGiLTBkfWPLOfNYqjbeqAR+OGZQoS6hUjbBYU7QuFa4FOYBHkNB2UqNsdsMb9qB/qs7QGTSb8Lok5YjW1c81BUpmIyKvuqnKma0MZskrpTYGQD2eJDABFCZwLFm+LgDyUTeSiV5xguYztLrHOk8LHKo9M8dIZgoBjeFVJxyjbcXKsVS3aQkMXKCrRlKLqhZTws/ZJwVfW9DbktZ9dT+tRZQvI7tjJofojcLX61AGJDnqUf5+2Gv1tEnmUI953gIzc8NlcFabPOsDsZEODt7MdOCTPT3w29umyhKbCsslpb64LoS/AB2QRPRCgkJS7snRA",
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
"mac": "lEfHlqfJQwU"
}
};
/** A test clear event */
export const BOB_CLEAR_EVENT: Partial<IEvent> = {
"type": "m.room.message",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"msgtype": "m.text",
"body": "Hello world"
}
};
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
"type": "m.room.encrypted",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"ciphertext": "AwgAEnA/mEqZm2lSrhoG11OpDqsohGSBJWsudbuoItLlivmpFZQHrKMbE6z/dhCTwUi76vwfRCtf4tyPMD845cqZH1nL0bowq3/awyzZ8Q263Y3WrLfkUTFBU6oPF/IULUFZZuw6kLdfd5g5+uigvqUhFFpICoj7KNHznv4sFNssd00/WgJquZ6PRt6e1v6ANFNiZPAwghIL+kBc6pb8i6MUWt9JnXilJhTqFDHdXiY4qkaKBWbwebC26PYM",
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
"device_id": "TEST_DEVICE"
},
"event_id": "$event1",
"origin_server_ts": 1507753886000
};
+151 -5
View File
@@ -6,9 +6,19 @@ import "../olm-loader";
import { logger } from "../../src/logger";
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import {
ClientEvent,
EventType,
IJoinedRoom,
IPusher,
ISyncResponse,
MatrixClient,
MsgType,
RelationType,
} from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
import { TEST_ROOM_ID } from "./test-data";
/**
* Return a promise that is resolved when the client next emits a
@@ -39,6 +49,62 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
});
}
/**
* Return a sync response which contains a single room (by default TEST_ROOM_ID), with the members given
* @param roomMembers
* @param roomId
*
* @returns the sync response
*/
export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): ISyncResponse {
const roomResponse: IJoinedRoom = {
summary: {
"m.heroes": [],
"m.joined_member_count": roomMembers.length,
"m.invited_member_count": roomMembers.length,
},
state: {
events: [
mkEventCustom({
sender: roomMembers[0],
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
}),
],
},
timeline: {
events: [],
prev_batch: "",
},
ephemeral: { events: [] },
account_data: { events: [] },
unread_notifications: {},
};
for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
mkMembershipCustom({
membership: "join",
sender: roomMembers[i],
}),
);
}
return {
next_batch: "1",
rooms: {
join: { [roomId]: roomResponse },
invite: {},
leave: {},
knock: {},
},
account_data: { events: [] },
};
}
/**
* Create a spy for an object and automatically spy its methods.
* @param constr - The class constructor (used with 'new')
@@ -258,6 +324,9 @@ export interface IMessageOpts {
* @param opts.user - The user ID for the event.
* @param opts.msg - Optional. The content.body for the event.
* @param opts.event - True to make a MatrixEvent.
* @param opts.relatesTo - An IEventRelation relating this to another event.
* @param opts.ts - The timestamp of the event.
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns The event
*/
@@ -297,6 +366,7 @@ interface IReplyMessageOpts extends IMessageOpts {
* @param opts.room - The room ID for the event.
* @param opts.user - The user ID for the event.
* @param opts.msg - Optional. The content.body for the event.
* @param opts.ts - The timestamp of the event.
* @param opts.replyToMessage - The replied message
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
@@ -330,6 +400,73 @@ export function mkReplyMessage(
return mkEvent(eventOpts, client);
}
/**
* Create a reaction event.
*
* @param target - the event we are reacting to.
* @param client - the MatrixClient
* @param userId - the userId of the sender
* @param roomId - the id of the room we are in
* @param ts - The timestamp of the event.
* @returns The event
*/
export function mkReaction(
target: MatrixEvent,
client: MatrixClient,
userId: string,
roomId: string,
ts?: number,
): MatrixEvent {
return mkEvent(
{
event: true,
type: EventType.Reaction,
user: userId,
room: roomId,
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: target.getId()!,
key: Math.random().toString(),
},
},
ts,
},
client,
);
}
export function mkEdit(
target: MatrixEvent,
client: MatrixClient,
userId: string,
roomId: string,
msg?: string,
ts?: number,
) {
msg = msg ?? `Edit of ${target.getId()}`;
return mkEvent(
{
event: true,
type: EventType.RoomMessage,
user: userId,
room: roomId,
content: {
"body": `* ${msg}`,
"m.new_content": {
body: msg,
},
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: target.getId()!,
},
},
ts,
},
client,
);
}
/**
* A mock implementation of webstorage
*/
@@ -384,10 +521,19 @@ export async function awaitDecryption(
}
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
resolve(ev);
});
if (waitOnDecryptionFailure) {
event.on(MatrixEventEvent.Decrypted, (ev, err) => {
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
if (!err) {
resolve(ev);
}
});
} else {
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
resolve(ev);
});
}
});
}
+24 -4
View File
@@ -18,7 +18,7 @@ import { RelationType } from "../../src/@types/event";
import { MatrixClient } from "../../src/client";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Thread } from "../../src/models/thread";
import { Thread, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { mkMessage } from "./test-utils";
export const makeThreadEvent = ({
@@ -34,7 +34,7 @@ export const makeThreadEvent = ({
...props,
relatesTo: {
event_id: rootEventId,
rel_type: "m.thread",
rel_type: THREAD_RELATION_TYPE.name,
["m.in_reply_to"]: {
event_id: replyToEventId,
},
@@ -115,6 +115,26 @@ type MakeThreadProps = {
ts?: number;
};
type MakeThreadResult = {
/**
* Thread model
*/
thread: Thread;
/**
* Thread root event
*/
rootEvent: MatrixEvent;
/**
* Events added to the thread
*/
events: MatrixEvent[];
};
/**
* Starts a new thread in a room by creating a message as thread root.
* Also creates a Thread model and adds it to the room.
* Does not insert the messages into a timeline.
*/
export const mkThread = ({
room,
client,
@@ -122,7 +142,7 @@ export const mkThread = ({
participantUserIds,
length = 2,
ts = 1,
}: MakeThreadProps): { thread: Thread; rootEvent: MatrixEvent; events: MatrixEvent[] } => {
}: MakeThreadProps): MakeThreadResult => {
const { rootEvent, events } = makeThreadEvents({
roomId: room.roomId,
authorId,
@@ -137,7 +157,7 @@ export const mkThread = ({
room?.reEmitter.reEmit(evt, [MatrixEventEvent.BeforeRedaction]);
}
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, [rootEvent, ...events], true);
return { thread, rootEvent, events };
};
+76
View File
@@ -485,6 +485,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();
public getFoci = jest.fn();
public supportsThreads(): boolean {
return true;
@@ -745,3 +746,78 @@ export const REMOTE_SFU_DESCRIPTION =
"a=sctp-port:5000\n" +
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs";
export const groupCallParticipantsFourOtherDevices = new Map([
[
new RoomMember("roomId0", "user1"),
new Map([
[
"deviceId0",
{
sessionId: "0",
screensharing: false,
},
],
[
"deviceId1",
{
sessionId: "1",
screensharing: false,
},
],
[
"deviceId2",
{
sessionId: "2",
screensharing: false,
},
],
]),
],
[
new RoomMember("roomId0", "user2"),
new Map([
[
"deviceId3",
{
sessionId: "0",
screensharing: false,
},
],
[
"deviceId4",
{
sessionId: "1",
screensharing: false,
},
],
]),
],
]);
export const groupCallParticipantsOneOtherDevice = new Map([
[
new RoomMember("roomId1", "thisMember"),
new Map([
[
"deviceId0",
{
sessionId: "0",
screensharing: false,
},
],
]),
],
[
new RoomMember("roomId1", "opponentMember"),
new Map([
[
"deviceId1",
{
sessionId: "1",
screensharing: false,
},
],
]),
],
]);
File diff suppressed because it is too large Load Diff
+186 -12
View File
@@ -15,9 +15,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import MockHttpBackend from "matrix-mock-request";
import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src";
import { AutoDiscovery } from "../../src/autodiscovery";
import { OidcError } from "../../src/oidc/error";
import { makeDelegatedAuthConfig } from "../test-utils/oidc";
// keep to reset the fetch function after using MockHttpBackend
// @ts-ignore private property
const realAutoDiscoveryFetch: typeof global.fetch = AutoDiscovery.fetchFn;
describe("AutoDiscovery", function () {
const getHttpBackend = (): MockHttpBackend => {
@@ -26,6 +34,10 @@ describe("AutoDiscovery", function () {
return httpBackend;
};
afterAll(() => {
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
});
it("should throw an error when no domain is specified", function () {
getHttpBackend();
return Promise.all([
@@ -339,7 +351,7 @@ describe("AutoDiscovery", function () {
function () {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
not_matrix_versions: ["r0.0.1"],
not_matrix_versions: ["v1.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -368,7 +380,7 @@ describe("AutoDiscovery", function () {
},
);
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " + "m.homeserver", 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")
@@ -376,7 +388,7 @@ describe("AutoDiscovery", function () {
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -397,6 +409,10 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -412,7 +428,7 @@ describe("AutoDiscovery", function () {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -434,6 +450,54 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS with authentication error when authentication config is invalid", function () {
const httpBackend = getHttpBackend();
httpBackend
.when("GET", "/_matrix/client/versions")
.check((req) => {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["v1.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.authentication": {
invalid: true,
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
"m.authentication": {
state: "FAIL_ERROR",
error: OidcError.Misconfigured,
},
};
expect(conf).toEqual(expected);
@@ -451,7 +515,7 @@ describe("AutoDiscovery", function () {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -496,7 +560,7 @@ describe("AutoDiscovery", function () {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -542,7 +606,7 @@ describe("AutoDiscovery", function () {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
@@ -589,7 +653,7 @@ describe("AutoDiscovery", function () {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
@@ -625,7 +689,7 @@ describe("AutoDiscovery", function () {
},
);
it("should return SUCCESS when the identity server configuration is " + "verifiably accurate", function () {
it("should return SUCCESS when the identity server configuration is verifiably accurate", function () {
const httpBackend = getHttpBackend();
httpBackend
.when("GET", "/_matrix/client/versions")
@@ -633,7 +697,7 @@ describe("AutoDiscovery", function () {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
httpBackend
.when("GET", "/_matrix/identity/v2")
@@ -664,6 +728,10 @@ describe("AutoDiscovery", function () {
error: null,
base_url: "https://identity.example.org",
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -671,7 +739,7 @@ describe("AutoDiscovery", function () {
]);
});
it("should return SUCCESS and preserve non-standard keys from the " + ".well-known response", 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")
@@ -679,7 +747,7 @@ describe("AutoDiscovery", function () {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
httpBackend
.when("GET", "/_matrix/identity/v2")
@@ -716,6 +784,10 @@ describe("AutoDiscovery", function () {
"org.example.custom.property": {
cupcakes: "yes",
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -794,4 +866,106 @@ describe("AutoDiscovery", function () {
}),
]);
});
it("should FAIL_ERROR for unsupported Matrix version", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
versions: ["r0.6.0"],
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: AutoDiscoveryAction.FAIL_ERROR,
error: AutoDiscovery.ERROR_HOMESERVER_TOO_OLD,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
describe("m.authentication", () => {
const homeserverName = "example.org";
const homeserverUrl = "https://chat.example.org/";
const issuer = "https://auth.org/";
beforeAll(() => {
// make these tests independent from fetch mocking above
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
});
beforeEach(() => {
fetchMock.resetBehavior();
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.1"] });
fetchMock.get("https://example.org/.well-known/matrix/client", {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.authentication": {
issuer,
},
});
});
it("should return valid authentication configuration", async () => {
const config = makeDelegatedAuthConfig(issuer);
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
const result = await AutoDiscovery.findClientConfig(homeserverName);
expect(result[M_AUTHENTICATION.stable!]).toEqual({
state: AutoDiscovery.SUCCESS,
...config,
signingKeys: [],
account: undefined,
error: null,
});
});
it("should set state to error for invalid authentication configuration", async () => {
const config = makeDelegatedAuthConfig(issuer);
// authorization_code is required
config.metadata.grant_types_supported = ["openid"];
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
const result = await AutoDiscovery.findClientConfig(homeserverName);
expect(result[M_AUTHENTICATION.stable!]).toEqual({
state: AutoDiscovery.FAIL_ERROR,
error: OidcError.OpSupport,
});
});
});
});
+4 -4
View File
@@ -33,7 +33,7 @@ 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",
baseUrl + "/_matrix/media/v3/download/server.name/resourceid",
);
});
@@ -44,21 +44,21 @@ describe("ContentRepo", function () {
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" + "?width=32&height=64&method=crop",
baseUrl + "/_matrix/media/v3/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" + "?width=32#automade",
baseUrl + "/_matrix/media/v3/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",
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
);
});
});
+131 -32
View File
@@ -15,11 +15,16 @@ import { sleep } from "../../src/utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from "../../src/logger";
import { MemoryStore } from "../../src";
import { DeviceVerification, MemoryStore } from "../../src";
import { RoomKeyRequestState } from "../../src/crypto/OutgoingRoomKeyRequestManager";
import { RoomMember } from "../../src/models/room-member";
import { IStore } from "../../src/store";
import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList";
import { EventShieldColour, EventShieldReason } from "../../src/crypto-api";
import { UserTrustLevel } from "../../src/crypto/CrossSigning";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
import * as testData from "../test-utils/test-data";
const Olm = global.Olm;
@@ -111,13 +116,14 @@ describe("Crypto", function () {
});
describe("encrypted events", function () {
it("provides encryption information", async function () {
it("provides encryption information for events from unverified senders", async function () {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
// unencrypted event
const event = {
getId: () => "$event_id",
getSender: () => "@bob:example.com",
getSenderKey: () => null,
getWireContent: () => {
return {};
@@ -127,6 +133,8 @@ describe("Crypto", function () {
let encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeFalsy();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toBe(null);
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
event.getSenderKey = () => "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI";
event.getWireContent = () => {
@@ -141,6 +149,11 @@ describe("Crypto", function () {
expect(encryptionInfo.authenticated).toBeFalsy();
expect(encryptionInfo.sender).toBeFalsy();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
});
// known sender, megolm key from backup
event.getForwardingCurve25519KeyChain = () => [];
event.isKeySourceUntrusted = () => true;
@@ -155,6 +168,11 @@ describe("Crypto", function () {
expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeFalsy();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
});
// known sender, trusted megolm key, but bad ed25519key
event.isKeySourceUntrusted = () => false;
device.keys["ed25519:FLIBBLE"] = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
@@ -165,9 +183,115 @@ describe("Crypto", function () {
expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeTruthy();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.MISMATCHED_SENDER_KEY,
});
client.stopClient();
});
describe("provides encryption information for events from verified senders", function () {
const testDeviceId = testData.BOB_TEST_DEVICE_ID;
const testDevice = testData.BOB_SIGNED_TEST_DEVICE_DATA;
let client: MatrixClient;
beforeEach(async () => {
client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
// mock out the verification check
client.crypto!.checkUserTrust = (userId) => new UserTrustLevel(true, false, false);
});
afterEach(() => {
client.stopClient();
});
async function buildEncryptedEvent(
decryptionResult: Partial<EventDecryptionResult> = {},
): Promise<MatrixEvent> {
const mockCryptoBackend = {
decryptEvent: async (event: MatrixEvent): Promise<EventDecryptionResult> => {
return {
claimedEd25519Key: testDevice.keys["ed25519:" + testDeviceId],
clearEvent: {
room_id: "!room_id",
type: "m.room.message",
content: { body: "test" },
},
forwardingCurve25519KeyChain: [],
senderCurve25519Key: testDevice.keys["curve25519:" + testDeviceId],
...decryptionResult,
};
},
} as unknown as CryptoBackend;
const event = new MatrixEvent({
event_id: "$event_id",
sender: testData.BOB_TEST_USER_ID,
type: "m.room.encrypted",
content: { algorithm: "m.megolm.v1.aes-sha2" },
});
await event.attemptDecryption(mockCryptoBackend);
return event;
}
it("unknown device", async () => {
const event = await buildEncryptedEvent();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
});
});
it("known but unsigned device", async () => {
client.crypto!.deviceList.storeDevicesForUser(testData.BOB_TEST_USER_ID, {
[testDeviceId]: {
keys: testDevice.keys,
algorithms: testDevice.algorithms,
verified: DeviceVerification.Unverified,
known: true,
},
});
const event = await buildEncryptedEvent();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNVERIFIED_IDENTITY,
});
});
describe("known and verified device", () => {
beforeEach(() => {
client.crypto!.deviceList.storeDevicesForUser(testData.BOB_TEST_USER_ID, {
[testDeviceId]: {
keys: testDevice.keys,
algorithms: testDevice.algorithms,
verified: DeviceVerification.Verified,
known: true,
},
});
});
it("regular key", async () => {
const event = await buildEncryptedEvent();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.NONE,
shieldReason: null,
});
});
it("unauthenticated key", async () => {
const event = await buildEncryptedEvent({ untrusted: true });
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
});
});
});
});
it("doesn't throw an error when attempting to decrypt a redacted event", async () => {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
@@ -381,12 +505,7 @@ describe("Crypto", function () {
event.senderCurve25519Key = null;
// @ts-ignore private properties
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
}
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
}),
);
@@ -617,12 +736,7 @@ describe("Crypto", function () {
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await secondAliceClient.crypto!.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
await expect(secondAliceClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
}),
);
@@ -725,12 +839,7 @@ describe("Crypto", function () {
event.senderCurve25519Key = null;
// @ts-ignore private properties
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
}
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
}),
);
@@ -805,12 +914,7 @@ describe("Crypto", function () {
event.senderCurve25519Key = null;
// @ts-ignore private properties
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
}
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
}),
);
@@ -897,12 +1001,7 @@ describe("Crypto", function () {
event.senderCurve25519Key = null;
// @ts-ignore private properties
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
}
await expect(bobClient.crypto!.decryptEvent(event)).rejects.toBeTruthy();
}),
);
+5 -7
View File
@@ -129,7 +129,7 @@ describe("DeviceList", function () {
});
});
it("should have an outdated devicelist on an invalidation while an " + "update is in progress", function () {
it("should have an outdated devicelist on an invalidation while an update is in progress", async function () {
const dl = createTestDeviceList();
dl.startTrackingDeviceList("@test1:sw1v.org");
@@ -148,11 +148,8 @@ describe("DeviceList", function () {
dl.invalidateUserDeviceList("@test1:sw1v.org");
dl.refreshOutdatedDeviceLists();
// TODO: Fix this test so we actually await the call and assertions and remove
// the eslint disable, https://github.com/matrix-org/matrix-js-sdk/issues/2977
//
// eslint-disable-next-line jest/valid-expect-in-promise
dl.saveIfDirty()
await dl
.saveIfDirty()
.then(() => {
// the first request completes
queryDefer1.resolve({
@@ -163,12 +160,13 @@ describe("DeviceList", function () {
});
return prom1;
})
.then(() => {
.then(async () => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
logger.log("Creating new devicelist to simulate app reload");
downloadSpy.mockReset();
const dl2 = createTestDeviceList();
await dl2.load();
const queryDefer3 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer3.promise);
+52
View File
@@ -0,0 +1,52 @@
/*
Copyright 2023 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 { TextEncoder, TextDecoder } from "util";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64 } from "../../../src/common-crypto/base64";
describe("Crypto Base64 encoding", () => {
it("Should decode properly encoded data", async () => {
const toEncode = "encoding hello world";
const encoded = encodeBase64(new TextEncoder().encode(toEncode));
const decoded = new TextDecoder().decode(decodeBase64(encoded));
expect(decoded).toStrictEqual(toEncode);
});
it("Encode unpadded should not have padding", async () => {
const toEncode = "encoding hello world";
const data = new TextEncoder().encode(toEncode);
const paddedEncoded = encodeBase64(data);
const unpaddedEncoded = encodeUnpaddedBase64(data);
expect(paddedEncoded).not.toEqual(unpaddedEncoded);
const padding = paddedEncoded.charAt(paddedEncoded.length - 1);
expect(padding).toStrictEqual("=");
});
it("Decode should be indifferent to padding", async () => {
const withPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ=";
const withoutPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ";
const decodedPad = decodeBase64(withPadding);
const decodedNoPad = decodeBase64(withoutPadding);
expect(decodedPad).toStrictEqual(decodedNoPad);
});
});
+4 -14
View File
@@ -148,22 +148,14 @@ describe("Secrets", function () {
it("should throw if given a key that doesn't exist", async function () {
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
try {
await alice.storeSecret("foo", "bar", ["this secret does not exist"]);
// should be able to use expect(...).toThrow() but mocha still fails
// the test even when it throws for reasons I have no inclination to debug
expect(true).toBeFalsy();
} catch (e) {}
await expect(alice.storeSecret("foo", "bar", ["this secret does not exist"])).rejects.toBeTruthy();
alice.stopClient();
});
it("should refuse to encrypt with zero keys", async function () {
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
try {
await alice.storeSecret("foo", "bar", []);
expect(true).toBeFalsy();
} catch (e) {}
await expect(alice.storeSecret("foo", "bar", [])).rejects.toBeTruthy();
alice.stopClient();
});
@@ -214,10 +206,7 @@ describe("Secrets", function () {
it("should refuse to encrypt if no keys given and no default key", async function () {
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
try {
await alice.storeSecret("foo", "bar");
expect(true).toBeFalsy();
} catch (e) {}
await expect(alice.storeSecret("foo", "bar")).rejects.toBeTruthy();
alice.stopClient();
});
@@ -323,6 +312,7 @@ describe("Secrets", function () {
this.emit(ClientEvent.AccountData, event);
return {};
};
bob.getKeyBackupVersion = jest.fn().mockResolvedValue(null);
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (func) => {
+4 -4
View File
@@ -144,7 +144,7 @@ describe("SAS verification", function () {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
} catch {
e.mismatch();
aliceSasEvent.mismatch();
}
@@ -169,7 +169,7 @@ describe("SAS verification", function () {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
} catch {
e.mismatch();
bobSasEvent.mismatch();
}
@@ -519,7 +519,7 @@ describe("SAS verification", function () {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
} catch {
e.mismatch();
aliceSasEvent.mismatch();
}
@@ -543,7 +543,7 @@ describe("SAS verification", function () {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
} catch {
e.mismatch();
bobSasEvent.mismatch();
}
+27 -2
View File
@@ -23,7 +23,14 @@ limitations under the License.
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { MockedObject } from "jest-mock";
import { WidgetApi, WidgetApiToWidgetAction, MatrixCapabilities, ITurnServer, IRoomEvent } from "matrix-widget-api";
import {
WidgetApi,
WidgetApiToWidgetAction,
MatrixCapabilities,
ITurnServer,
IRoomEvent,
IOpenIDCredentials,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
@@ -33,6 +40,12 @@ import { MatrixEvent } from "../../src/models/event";
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
const testOIDCToken = {
access_token: "12345678",
expires_in: "10",
matrix_server_name: "homeserver.oabc",
token_type: "Bearer",
};
class MockWidgetApi extends EventEmitter {
public start = jest.fn();
public requestCapability = jest.fn();
@@ -49,8 +62,15 @@ class MockWidgetApi extends EventEmitter {
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
public sendStateEvent = jest.fn();
public sendToDevice = jest.fn();
public requestOpenIDConnectToken = jest.fn(() => {
return testOIDCToken;
return new Promise<IOpenIDCredentials>(() => {
return testOIDCToken;
});
});
public readStateEvents = jest.fn(() => []);
public getTurnServers = jest.fn(() => []);
public sendContentLoaded = jest.fn();
public transport = { reply: jest.fn() };
}
@@ -285,7 +305,12 @@ describe("RoomWidgetClient", () => {
expect(await emittedSync).toEqual(SyncState.Syncing);
});
});
describe("oidc token", () => {
it("requests an oidc token", async () => {
await makeClient({});
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
});
});
it("gets TURN servers", async () => {
const server1: ITurnServer = {
uris: [
+49 -94
View File
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils";
import {
DuplicateStrategy,
@@ -24,13 +26,10 @@ import {
MatrixClient,
MatrixEvent,
MatrixEventEvent,
RelationType,
Room,
RoomEvent,
} from "../../src";
import { FeatureSupport, Thread } from "../../src/models/thread";
import { Thread } from "../../src/models/thread";
import { ReEmitter } from "../../src/ReEmitter";
import { eventMapperFor } from "../../src/event-mapper";
describe("EventTimelineSet", () => {
const roomId = "!foo:bar";
@@ -145,13 +144,6 @@ describe("EventTimelineSet", () => {
});
describe("addEventToTimeline", () => {
let thread: Thread;
beforeEach(() => {
(client.supportsThreads as jest.Mock).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
it("Adds event to timeline", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
@@ -171,6 +163,42 @@ describe("EventTimelineSet", () => {
}).not.toThrow();
});
it("should aggregate relations which belong to unknown timeline without adding them to any timeline", () => {
// If threads are disabled all events go into the main timeline
mocked(client.supportsThreads).mockReturnValue(true);
const reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(reactionEvent, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents()).toHaveLength(1);
const [event] = liveTimeline.getEvents();
const reactions = eventTimelineSet.relations!.getChildEventsForEvent(
event.getId()!,
"m.annotation",
"m.reaction",
)!;
const relations = reactions.getRelations();
expect(relations).toHaveLength(1);
expect(relations[0].getId()).toBe(reactionEvent.getId());
});
});
describe("addEventToTimeline (thread timeline)", () => {
let thread: Thread;
beforeEach(() => {
(client.supportsThreads as jest.Mock).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
it("should not add an event to a timeline that does not belong to the timelineSet", () => {
const eventTimelineSet2 = new EventTimelineSet(room);
const liveTimeline2 = eventTimelineSet2.getLiveTimeline();
@@ -200,94 +228,19 @@ describe("EventTimelineSet", () => {
const liveTimeline = eventTimelineSetForThread.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, {
const normalMessage = utils.mkMessage({
room: roomId,
user: userA,
msg: "Hello!",
event: true,
});
eventTimelineSetForThread.addEventToTimeline(normalMessage, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
it("should allow edits to be added to thread timeline", async () => {
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
Thread.hasServerSideSupport = FeatureSupport.Stable;
const sender = "@alice:matrix.org";
const root = utils.mkEvent({
event: true,
content: {
body: "Thread root",
},
type: EventType.RoomMessage,
sender,
});
room.addLiveEvents([root]);
const threadReply = utils.mkEvent({
event: true,
content: {
"body": "Thread reply",
"m.relates_to": {
event_id: root.getId()!,
rel_type: RelationType.Thread,
},
},
type: EventType.RoomMessage,
sender,
});
root.setUnsigned({
"m.relations": {
[RelationType.Thread]: {
count: 1,
latest_event: {
content: threadReply.getContent(),
origin_server_ts: 5,
room_id: room.roomId,
sender,
type: EventType.RoomMessage,
event_id: threadReply.getId()!,
user_id: sender,
age: 1,
},
current_user_participated: true,
},
},
});
const editToThreadReply = utils.mkEvent({
event: true,
content: {
"body": " * edit",
"m.new_content": {
"body": "edit",
"msgtype": "m.text",
"org.matrix.msc1767.text": "edit",
},
"m.relates_to": {
event_id: threadReply.getId()!,
rel_type: RelationType.Replace,
},
},
type: EventType.RoomMessage,
sender,
});
jest.spyOn(client, "paginateEventTimeline").mockImplementation(async () => {
thread.timelineSet.getLiveTimeline().addEvent(threadReply, { toStartOfTimeline: true });
return true;
});
jest.spyOn(client, "relations").mockResolvedValue({
events: [],
});
const thread = room.createThread(root.getId()!, root, [threadReply, editToThreadReply], false);
thread.once(RoomEvent.TimelineReset, () => {
const lastEvent = thread.timeline.at(-1)!;
expect(lastEvent.getContent().body).toBe(" * edit");
});
});
describe("non-room timeline", () => {
it("Adds event to timeline", () => {
const nonRoomEventTimelineSet = new EventTimelineSet(
@@ -421,7 +374,9 @@ describe("EventTimelineSet", () => {
});
it("should return true if the timeline set is not for a thread and the event is a thread root", () => {
const thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
const eventTimelineSet = new EventTimelineSet(room, {}, client);
messageEvent.setThread(thread);
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
});
@@ -6,6 +6,6 @@ exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`]
"params": {
"access_token": "token",
},
"path": "/_matrix/media/r0/upload",
"path": "/_matrix/media/v3/upload",
}
`;
+86
View File
@@ -14,10 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
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";
import { defer, QueryDict } from "../../../src/utils";
import { logger } from "../../../src/logger";
describe("FetchHttpApi", () => {
const baseUrl = "http://baseUrl";
@@ -235,4 +239,86 @@ describe("FetchHttpApi", () => {
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
});
});
describe("getUrl()", () => {
const localBaseUrl = "http://baseurl";
const baseUrlWithTrailingSlash = "http://baseurl/";
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
const fetchFn = jest.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn });
};
type TestParams = {
path: string;
queryParams?: QueryDict;
prefix?: string;
baseUrl?: string;
};
type TestCase = [TestParams, string];
const queryParams: QueryDict = {
test1: 99,
test2: ["a", "b"],
};
const testPrefix = "/just/testing";
const testUrl = "http://justtesting.com";
const testUrlWithTrailingSlash = "http://justtesting.com/";
const testCases: TestCase[] = [
[{ path: "/terms" }, `${localBaseUrl}${prefix}/terms`],
[{ path: "/terms", queryParams }, `${localBaseUrl}${prefix}/terms?test1=99&test2=a&test2=b`],
[{ path: "/terms", prefix: testPrefix }, `${localBaseUrl}${testPrefix}/terms`],
[{ path: "/terms", baseUrl: testUrl }, `${testUrl}${prefix}/terms`],
[{ path: "/terms", baseUrl: testUrlWithTrailingSlash }, `${testUrl}${prefix}/terms`],
[
{ path: "/terms", queryParams, prefix: testPrefix, baseUrl: testUrl },
`${testUrl}${testPrefix}/terms?test1=99&test2=a&test2=b`,
],
];
const runTests = (fetchBaseUrl: string) => {
it.each<TestCase>(testCases)(
"creates url with params %s => %s",
({ path, queryParams, prefix, baseUrl }, expected) => {
const api = makeApi(fetchBaseUrl);
const result = api.getUrl(path, queryParams, prefix, baseUrl);
// we only check the stringified URL, to avoid having the test depend on the internals of URL.
expect(result.toString()).toEqual(expected);
},
);
};
describe("when fetch.opts.baseUrl does not have a trailing slash", () => {
runTests(localBaseUrl);
});
describe("when fetch.opts.baseUrl does have a trailing slash", () => {
runTests(baseUrlWithTrailingSlash);
});
});
it("should not log query parameters", async () => {
jest.useFakeTimers();
const deferred = defer<Response>();
const fetchFn = jest.fn().mockReturnValue(deferred.promise);
jest.spyOn(logger, "debug").mockImplementation(() => {});
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
jest.advanceTimersByTime(1234);
deferred.resolve({ ok: true, status: 200, text: () => Promise.resolve("RESPONSE") } as Response);
await prom;
expect(logger.debug).not.toHaveBeenCalledWith("fragment");
expect(logger.debug).not.toHaveBeenCalledWith("query");
expect(logger.debug).not.toHaveBeenCalledWith("param");
expect(logger.debug).toHaveBeenCalledTimes(2);
expect(mocked(logger.debug).mock.calls[0]).toMatchInlineSnapshot(`
[
"FetchHttpApi: --> GET https://server:8448/some/path?query=xxx",
]
`);
expect(mocked(logger.debug).mock.calls[1]).toMatchInlineSnapshot(`
[
"FetchHttpApi: <-- GET https://server:8448/some/path?query=xxx [1234ms 200]",
]
`);
});
});
+4 -4
View File
@@ -84,7 +84,7 @@ describe("MatrixHttpApi", () => {
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token",
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token",
);
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
});
@@ -96,7 +96,7 @@ describe("MatrixHttpApi", () => {
accessToken: "token",
});
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
});
@@ -105,14 +105,14 @@ describe("MatrixHttpApi", () => {
upload = api.uploadContent({} as File, { name: "name" });
expect(xhr.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name",
baseUrl.toLowerCase() + "/_matrix/media/v3/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");
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
});
it("should abort xhr when the upload is aborted", () => {
+85 -2
View File
@@ -94,7 +94,6 @@ describe("InteractiveAuth", () => {
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Password] }],
errcode: "MockError0",
params: {
[AuthType.Password]: { param: "aa" },
},
@@ -376,7 +375,7 @@ describe("InteractiveAuth", () => {
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 () => {
it("should handle unexpected error types without data property set", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
@@ -517,4 +516,88 @@ describe("InteractiveAuth", () => {
expect(ia.getEmailSid()).toEqual(sid);
});
});
it("should prioritise shorter flows", 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.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
params: {},
},
});
// @ts-ignore
ia.chooseStage();
expect(ia.getChosenFlow()?.stages).toEqual([AuthType.Password]);
});
it("should prioritise flows with entirely supported stages", 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: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }],
params: {},
},
supportedStages: [AuthType.Password],
});
// @ts-ignore
ia.chooseStage();
expect(ia.getChosenFlow()?.stages).toEqual([AuthType.Password]);
});
it("should fire stateUpdated callback with error when a request fails", 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" },
},
},
});
// StateUpdated should be called. We call submitAuthDict() to trigger a request ...
let firstTime = true;
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
// Only trigger the request the first time, to avoid an infinite loop
if (firstTime) {
firstTime = false;
ia.submitAuthDict({
type: AuthType.Password,
});
}
});
// .. which which we then reject, so we can test the behaviour in that case.
doRequest.mockRejectedValue(new MatrixError({ errcode: "M_UNKNOWN", error: "This is an error" }));
await Promise.allSettled([ia.attemptAuth()]);
expect(stateUpdated).toHaveBeenCalledWith("m.login.password", {
errcode: "M_UNKNOWN",
error: "This is an error",
});
});
});
+90
View File
@@ -1,6 +1,15 @@
import fetchMock from "fetch-mock-jest";
import { ClientPrefix, MatrixClient } from "../../src";
import { SSOAction } from "../../src/@types/auth";
import { TestClient } from "../TestClient";
function createExampleMatrixClient(): MatrixClient {
return new MatrixClient({
baseUrl: "https://example.com",
});
}
describe("Login request", function () {
let client: TestClient;
@@ -57,3 +66,84 @@ describe("SSO login URL", function () {
});
});
});
describe("refreshToken", () => {
afterEach(() => {
fetchMock.mockReset();
});
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
const client = createExampleMatrixClient();
const response = {
access_token: "access_token",
refresh_token: "refresh_token",
expires_in_ms: 30000,
};
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), response);
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), () => {
throw new Error("/v1/refresh unexpectedly called");
});
const refreshResult = await client.refreshToken("initial_refresh_token");
expect(refreshResult).toEqual(response);
});
it("falls back to /v1 when server does not recognized /v3 refresh", async () => {
const client = createExampleMatrixClient();
const response = {
access_token: "access_token",
refresh_token: "refresh_token",
expires_in_ms: 30000,
};
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), {
status: 400,
body: { errcode: "M_UNRECOGNIZED" },
});
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), response);
const refreshResult = await client.refreshToken("initial_refresh_token");
expect(refreshResult).toEqual(response);
});
it("re-raises M_UNRECOGNIZED exceptions from /v1", async () => {
const client = createExampleMatrixClient();
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), {
status: 400,
body: { errcode: "M_UNRECOGNIZED" },
});
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), {
status: 400,
body: { errcode: "M_UNRECOGNIZED" },
});
await expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ errcode: "M_UNRECOGNIZED" });
});
it("re-raises non-M_UNRECOGNIZED exceptions from /v3", async () => {
const client = createExampleMatrixClient();
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), 429);
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), () => {
throw new Error("/v1/refresh unexpectedly called");
});
await expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
});
it("re-raises non-M_UNRECOGNIZED exceptions from /v1", async () => {
const client = createExampleMatrixClient();
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), {
status: 400,
body: { errcode: "M_UNRECOGNIZED" },
});
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), 429);
await expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
});
});
+31 -33
View File
@@ -28,7 +28,6 @@ import {
UNSTABLE_MSC3088_ENABLED,
UNSTABLE_MSC3088_PURPOSE,
UNSTABLE_MSC3089_TREE_SUBTYPE,
MSC3912_RELATION_BASED_REDACTIONS_PROP,
} from "../../src/@types/event";
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
import { Crypto } from "../../src/crypto";
@@ -181,9 +180,7 @@ describe("MatrixClient", function () {
data: SYNC_DATA,
};
const unstableFeatures: Record<string, boolean> = {
"org.matrix.msc3440.stable": true,
};
let unstableFeatures: Record<string, boolean> = {};
// items are popped off when processed and block if no items left.
let httpLookups: HttpLookup[] = [];
@@ -204,7 +201,7 @@ describe("MatrixClient", function () {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return Promise.resolve({
unstable_features: unstableFeatures,
versions: ["r0.6.0", "r0.6.1"],
versions: ["v1.1"],
});
}
const next = httpLookups.shift();
@@ -342,6 +339,12 @@ 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));
// set unstableFeatures to a defined state before each test
unstableFeatures = {
"org.matrix.msc3440.stable": true,
};
makeClient();
// set reasonable working defaults
@@ -1373,10 +1376,10 @@ describe("MatrixClient", function () {
await client.redactEvent(roomId, eventId, txnId, { reason });
});
describe("when calling with with_relations", () => {
describe("when calling with 'with_rel_types'", () => {
const eventId = "$event42:example.org";
it("should raise an error if server has no support for relation based redactions", async () => {
it("should raise an error if the server has no support for relation based redactions", async () => {
// load supported features
await client.getVersions();
@@ -1384,7 +1387,7 @@ describe("MatrixClient", function () {
expect(() => {
client.redactEvent(roomId, eventId, txnId, {
with_relations: [RelationType.Reference],
with_rel_types: [RelationType.Reference],
});
}).toThrow(
new Error(
@@ -1394,34 +1397,30 @@ describe("MatrixClient", function () {
);
});
describe("and the server supports relation based redactions (unstable)", () => {
beforeEach(async () => {
unstableFeatures["org.matrix.msc3912"] = true;
// load supported features
await client.getVersions();
});
it("and the server has unstable support for relation based redactions, it should send 'org.matrix.msc3912.with_relations' in the request body", async () => {
unstableFeatures["org.matrix.msc3912"] = true;
// load supported features
await client.getVersions();
it("should send with_relations in the request body", async () => {
const txnId = client.makeTxnId();
const txnId = client.makeTxnId();
httpLookups = [
{
method: "PUT",
path:
`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}` +
`/${encodeURIComponent(txnId)}`,
expectBody: {
reason: "redaction test",
[MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable!]: [RelationType.Reference],
},
data: { event_id: eventId },
httpLookups = [
{
method: "PUT",
path:
`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}` +
`/${encodeURIComponent(txnId)}`,
expectBody: {
reason: "redaction test",
["org.matrix.msc3912.with_relations"]: ["m.reference"],
},
];
data: { event_id: eventId },
},
];
await client.redactEvent(roomId, eventId, txnId, {
reason: "redaction test",
with_relations: [RelationType.Reference],
});
await client.redactEvent(roomId, eventId, txnId, {
reason: "redaction test",
with_rel_types: [RelationType.Reference],
});
});
});
@@ -2267,7 +2266,6 @@ describe("MatrixClient", function () {
function roomCreateEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent {
return new MatrixEvent({
content: {
"creator": "@daryl:alexandria.example.com",
"m.federate": true,
"predecessor": {
event_id: "id_of_last_event",
+139
View File
@@ -0,0 +1,139 @@
/*
Copyright 2023 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 { MatrixEvent } from "../../../src";
import { CallMembership, CallMembershipData } from "../../../src/matrixrtc/CallMembership";
const membershipTemplate: CallMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 5000,
};
function makeMockEvent(originTs = 0): MatrixEvent {
return {
getTs: jest.fn().mockReturnValue(originTs),
sender: {
userId: "@alice:example.org",
},
} as unknown as MatrixEvent;
}
describe("CallMembership", () => {
it("rejects membership with no expiry", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: undefined }));
}).toThrow();
});
it("rejects membership with no device_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
}).toThrow();
});
it("rejects membership with no call_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
}).toThrow();
});
it("rejects membership with no scope", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
}).toThrow();
});
it("uses event timestamp if no created_ts", () => {
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
expect(membership.createdTs()).toEqual(12345);
});
it("uses created_ts if present", () => {
const membership = new CallMembership(
makeMockEvent(12345),
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
);
expect(membership.createdTs()).toEqual(67890);
});
it("computes absolute expiry time", () => {
const membership = new CallMembership(makeMockEvent(1000), membershipTemplate);
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
});
it("considers memberships unexpired if local age low enough", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
const membership = new CallMembership(fakeEvent, membershipTemplate);
expect(membership.isExpired()).toEqual(false);
});
it("considers memberships expired when local age large", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getLocalAge = jest.fn().mockReturnValue(6000);
const membership = new CallMembership(fakeEvent, membershipTemplate);
expect(membership.isExpired()).toEqual(true);
});
it("returns active foci", () => {
const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" };
const membership = new CallMembership(
fakeEvent,
Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }),
);
expect(membership.getActiveFoci()).toEqual([mockFocus]);
});
describe("expiry calculation", () => {
let fakeEvent: MatrixEvent;
let membership: CallMembership;
beforeEach(() => {
// server origin timestamp for this event is 1000
fakeEvent = makeMockEvent(1000);
// our clock would have been at 2000 at the creation time (our clock at event receive time - age)
// (ie. the local clock is 1 second ahead of the servers' clocks)
fakeEvent.localTimestamp = 2000;
// for simplicity's sake, we say that the event's age is zero
fakeEvent.getLocalAge = jest.fn().mockReturnValue(0);
membership = new CallMembership(fakeEvent!, membershipTemplate);
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it("converts expiry time into local clock", () => {
// for sanity's sake, make sure the server-relative expiry time is what we expect
expect(membership.getAbsoluteExpiry()).toEqual(6000);
// therefore the expiry time converted to our clock should be 1 second later
expect(membership.getLocalExpiry()).toEqual(7000);
});
it("calculates time until expiry", () => {
jest.setSystemTime(2000);
expect(membership.getMsUntilExpiry()).toEqual(5000);
});
});
});
@@ -0,0 +1,405 @@
/*
Copyright 2023 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 { EventTimeline, EventType, MatrixClient, Room } from "../../../src";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { randomString } from "../../../src/randomstring";
import { makeMockRoom, mockRTCEvent } from "./mocks";
const membershipTemplate: CallMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
};
const mockFocus = { type: "mock" };
describe("MatrixRTCSession", () => {
let client: MatrixClient;
let sess: MatrixRTCSession | undefined;
beforeEach(() => {
client = new MatrixClient({ baseUrl: "base_url" });
client.getUserId = jest.fn().mockReturnValue("@alice:example.org");
client.getDeviceId = jest.fn().mockReturnValue("AAAAAAA");
});
afterEach(() => {
client.stopClient();
client.matrixRTC.stop();
if (sess) sess.stop();
sess = undefined;
});
it("Creates a room-scoped session from room state", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(1);
expect(sess?.memberships[0].callId).toEqual("");
expect(sess?.memberships[0].scope).toEqual("m.room");
expect(sess?.memberships[0].application).toEqual("m.call");
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships[0].isExpired()).toEqual(false);
});
it("ignores expired memberships events", () => {
const expiredMembership = Object.assign({}, membershipTemplate);
expiredMembership.expires = 1000;
expiredMembership.device_id = "EXPIRED";
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], () => 10000);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(1);
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
});
it("honours created_ts", () => {
const expiredMembership = Object.assign({}, membershipTemplate);
expiredMembership.created_ts = 500;
expiredMembership.expires = 1000;
const mockRoom = makeMockRoom([expiredMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
});
it("returns empty session if no membership events are present", () => {
const mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships).toHaveLength(0);
});
it("safely ignores events with no memberships section", () => {
const mockRoom = {
roomId: randomString(8),
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({
getStateEvents: (_type: string, _stateKey: string) => [
{
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: jest.fn().mockReturnValue(0),
},
],
}),
}),
};
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room);
expect(sess.memberships).toHaveLength(0);
});
it("safely ignores events with junk memberships section", () => {
const mockRoom = {
roomId: randomString(8),
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({
getStateEvents: (_type: string, _stateKey: string) => [
{
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({ memberships: "i am a fish" }),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: jest.fn().mockReturnValue(0),
},
],
}),
}),
};
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room);
expect(sess.memberships).toHaveLength(0);
});
it("ignores memberships with no expires_ts", () => {
const expiredMembership = Object.assign({}, membershipTemplate);
(expiredMembership.expires as number | undefined) = undefined;
const mockRoom = makeMockRoom([expiredMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.memberships).toHaveLength(0);
});
it("ignores memberships with no device_id", () => {
const testMembership = Object.assign({}, membershipTemplate);
(testMembership.device_id as string | undefined) = undefined;
const mockRoom = makeMockRoom([testMembership]);
const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.memberships).toHaveLength(0);
});
it("ignores memberships with no call_id", () => {
const testMembership = Object.assign({}, membershipTemplate);
(testMembership.call_id as string | undefined) = undefined;
const mockRoom = makeMockRoom([testMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.memberships).toHaveLength(0);
});
it("ignores memberships with no scope", () => {
const testMembership = Object.assign({}, membershipTemplate);
(testMembership.scope as string | undefined) = undefined;
const mockRoom = makeMockRoom([testMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.memberships).toHaveLength(0);
});
it("ignores anything that's not a room-scoped call (for now)", () => {
const testMembership = Object.assign({}, membershipTemplate);
testMembership.scope = "m.user";
const mockRoom = makeMockRoom([testMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.memberships).toHaveLength(0);
});
describe("getOldestMembership", () => {
it("returns the oldest membership event", () => {
const mockRoom = makeMockRoom([
Object.assign({}, membershipTemplate, { device_id: "foo", created_ts: 3000 }),
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.getOldestMembership()!.deviceId).toEqual("old");
});
});
describe("joining", () => {
let mockRoom: Room;
beforeEach(() => {
mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
});
afterEach(() => {
// stop the timers
sess!.leaveRoomSession();
});
it("starts un-joined", () => {
expect(sess!.isJoined()).toEqual(false);
});
it("shows joined once join is called", () => {
sess!.joinRoomSession([mockFocus]);
expect(sess!.isJoined()).toEqual(true);
});
it("sends a membership event when joining a call", () => {
client.sendStateEvent = jest.fn();
sess!.joinRoomSession([mockFocus]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [{ type: "mock" }],
},
],
},
"@alice:example.org",
);
});
it("does nothing if join called when already joined", () => {
const sendStateEventMock = jest.fn();
client.sendStateEvent = sendStateEventMock;
sess!.joinRoomSession([mockFocus]);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
sess!.joinRoomSession([mockFocus]);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
});
it("renews membership event before expiry time", async () => {
jest.useFakeTimers();
let resolveFn: ((_roomId: string, _type: string, val: Record<string, any>) => void) | undefined;
const eventSentPromise = new Promise<Record<string, any>>((r) => {
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
r(val);
};
});
try {
const sendStateEventMock = jest.fn().mockImplementation(resolveFn);
client.sendStateEvent = sendStateEventMock;
sess!.joinRoomSession([mockFocus]);
const eventContent = await eventSentPromise;
// definitely should have renewed by 1 second before the expiry!
const timeElapsed = 60 * 60 * 1000 - 1000;
mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents = jest
.fn()
.mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, () => timeElapsed));
const eventReSentPromise = new Promise<Record<string, any>>((r) => {
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
r(val);
};
});
sendStateEventMock.mockReset().mockImplementation(resolveFn);
jest.setSystemTime(Date.now() + timeElapsed);
jest.advanceTimersByTime(timeElapsed);
await eventReSentPromise;
expect(sendStateEventMock).toHaveBeenCalledWith(
mockRoom.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000 * 2,
foci_active: [{ type: "mock" }],
created_ts: 1000,
},
],
},
"@alice:example.org",
);
} finally {
jest.useRealTimers();
}
});
});
it("emits an event at the time a membership event expires", () => {
jest.useFakeTimers();
try {
let eventAge = 0;
const membership = Object.assign({}, membershipTemplate);
const mockRoom = makeMockRoom([membership], () => eventAge);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const membershipObject = sess.memberships[0];
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
eventAge = 61 * 1000 * 1000;
jest.advanceTimersByTime(61 * 1000 * 1000);
expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []);
expect(sess?.memberships.length).toEqual(0);
} finally {
jest.useRealTimers();
}
});
it("prunes expired memberships on update", () => {
client.sendStateEvent = jest.fn();
let eventAge = 0;
const mockRoom = makeMockRoom(
[
Object.assign({}, membershipTemplate, {
device_id: "OTHERDEVICE",
expires: 1000,
}),
],
() => eventAge,
);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
// sanity check
expect(sess.memberships).toHaveLength(1);
expect(sess.memberships[0].deviceId).toEqual("OTHERDEVICE");
eventAge = 10000;
sess.joinRoomSession([mockFocus]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [mockFocus],
},
],
},
"@alice:example.org",
);
});
it("fills in created_ts for other memberships on update", () => {
client.sendStateEvent = jest.fn();
const mockRoom = makeMockRoom([
Object.assign({}, membershipTemplate, {
device_id: "OTHERDEVICE",
}),
]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.joinRoomSession([mockFocus]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "OTHERDEVICE",
expires: 3600000,
created_ts: 1000,
},
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [mockFocus],
},
],
},
"@alice:example.org",
);
});
});
@@ -0,0 +1,80 @@
/*
Copyright 2023 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 { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { makeMockRoom } from "./mocks";
const membershipTemplate: CallMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
};
describe("MatrixRTCSessionManager", () => {
let client: MatrixClient;
beforeEach(async () => {
client = new MatrixClient({ baseUrl: "base_url" });
client.matrixRTC.start();
});
afterEach(() => {
client.stopClient();
client.matrixRTC.stop();
});
it("Fires event when session starts", () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
try {
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
}
});
it("Fires event when session ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const memberships = [membershipTemplate];
const room1 = makeMockRoom(memberships);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
memberships.splice(0, 1);
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
});
+66
View File
@@ -0,0 +1,66 @@
/*
Copyright 2023 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 { EventType, MatrixEvent, Room } from "../../../src";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { randomString } from "../../../src/randomstring";
export function makeMockRoom(
memberships: CallMembershipData[],
getLocalAge: (() => number) | undefined = undefined,
): Room {
const roomId = randomString(8);
return {
roomId: roomId,
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue(makeMockRoomState(memberships, roomId, getLocalAge)),
}),
} as unknown as Room;
}
function makeMockRoomState(memberships: CallMembershipData[], roomId: string, getLocalAge: (() => number) | undefined) {
return {
getStateEvents: (_: string, stateKey: string) => {
const event = mockRTCEvent(memberships, roomId, getLocalAge);
if (stateKey !== undefined) return event;
return [event];
},
};
}
export function mockRTCEvent(
memberships: CallMembershipData[],
roomId: string,
getLocalAge: (() => number) | undefined,
): MatrixEvent {
const getLocalAgeFn = getLocalAge ?? (() => 10);
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({
memberships: memberships,
}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: getLocalAgeFn,
localTimestamp: Date.now(),
getRoomId: jest.fn().mockReturnValue(roomId),
sender: {
userId: "@mock:user.example",
},
} as unknown as MatrixEvent;
}
+21
View File
@@ -308,4 +308,25 @@ describe("MatrixEvent", () => {
});
});
});
it("should ignore thread relation on state events", async () => {
const stateEvent = new MatrixEvent({
event_id: "$event_id",
type: "some_state_event",
content: {
"foo": "bar",
"m.relates_to": {
"event_id": "$thread_id",
"m.in_reply_to": {
event_id: "$thread_id",
},
"rel_type": "m.thread",
},
},
state_key: "",
});
expect(stateEvent.isState()).toBeTruthy();
expect(stateEvent.threadRootId).toBeUndefined();
});
});
+309 -5
View File
@@ -14,17 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { MatrixClient, PendingEventOrdering } from "../../../src/client";
import { Room } from "../../../src/models/room";
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
import { mkThread } from "../../test-utils/thread";
import { Room, RoomEvent } from "../../../src/models/room";
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
import { makeThreadEvent, mkThread } from "../../test-utils/thread";
import { TestClient } from "../../TestClient";
import { emitPromise, mkMessage, mock } from "../../test-utils/test-utils";
import { Direction, EventStatus, MatrixEvent } from "../../../src";
import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils";
import { Direction, EventStatus, EventType, MatrixEvent } from "../../../src";
import { ReceiptType } from "../../../src/@types/read_receipts";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
import { ReEmitter } from "../../../src/ReEmitter";
import { Feature, ServerSupport } from "../../../src/feature";
import { eventMapperFor } from "../../../src/event-mapper";
describe("Thread", () => {
describe("constructor", () => {
@@ -40,6 +43,7 @@ describe("Thread", () => {
const myUserId = "@bob:example.org";
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { timelineSupport: false });
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
@@ -297,6 +301,7 @@ describe("Thread", () => {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
@@ -351,6 +356,7 @@ describe("Thread", () => {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
@@ -402,6 +408,7 @@ describe("Thread", () => {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
@@ -424,4 +431,301 @@ describe("Thread", () => {
expect(mock).toHaveBeenCalledWith("b1", "f1");
});
});
describe("insertEventIntoTimeline", () => {
it("Inserts a reaction in timestamp order", () => {
// Assumption: no server side support because if we have it, events
// can only be added to the timeline after the thread has been
// initialised, and we are not properly initialising it here.
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
const client = createClientWithEventMapper();
const userId = "user1";
const room = new Room("room1", client, userId);
// Given a thread with a root plus 5 messages
const { thread, events } = mkThread({
room,
client,
authorId: userId,
participantUserIds: ["@bob:hs", "@chia:hs", "@dv:hs"],
length: 6,
ts: 100, // Events will be at ts 100, 101, 102, 103, 104 and 105
});
// When we insert a reaction to the second thread message
const replyEvent = mkReaction(events[2], client, userId, room.roomId, 104);
thread.insertEventIntoTimeline(replyEvent);
// Then the reaction is inserted based on its timestamp
expect(thread.events.map((ev) => ev.getId())).toEqual([
events[0].getId(),
events[1].getId(),
events[2].getId(),
events[3].getId(),
events[4].getId(),
replyEvent.getId(),
events[5].getId(),
]);
});
describe("Without relations recursion support", () => {
it("Creates a local echo receipt for new events", async () => {
// Assumption: no server side support because if we have it, events
// can only be added to the timeline after the thread has been
// initialised, and we are not properly initialising it here.
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
// Given a client without relations recursion support
const client = createClientWithEventMapper();
// And a thread with an added event (with later timestamp)
const userId = "user1";
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
// Then a receipt was added to the thread
const receipt = thread.getReadReceiptForUserId(userId);
expect(receipt).toBeTruthy();
expect(receipt?.eventId).toEqual(message.getId());
expect(receipt?.data.ts).toEqual(100);
expect(receipt?.data.thread_id).toEqual(thread.id);
// (And the receipt was synthetic)
expect(thread.getReadReceiptForUserId(userId, true)).toBeNull();
});
it("Doesn't create a local echo receipt for events before an existing receipt", async () => {
// Assumption: no server side support because if we have it, events
// can only be added to the timeline after the thread has been
// initialised, and we are not properly initialising it here.
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
// Given a client without relations recursion support
const client = createClientWithEventMapper();
// And a thread with an added event with a lower timestamp than its other events
const userId = "user1";
const { thread } = await createThreadAndEvent(client, 200, 100, userId);
// Then no receipt was added to the thread (the receipt is still
// for the thread root). This happens because since we have no
// recursive relations support, we know that sometimes events
// appear out of order, so we have to check their timestamps as
// a guess of the correct order.
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId());
});
});
describe("With relations recursion support", () => {
it("Creates a local echo receipt for new events", async () => {
// Assumption: no server side support because if we have it, events
// can only be added to the timeline after the thread has been
// initialised, and we are not properly initialising it here.
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
// Given a client WITH relations recursion support
const client = createClientWithEventMapper(
new Map([[Feature.RelationsRecursion, ServerSupport.Stable]]),
);
// And a thread with an added event (with later timestamp)
const userId = "user1";
const { thread, message } = await createThreadAndEvent(client, 1, 100, userId);
// Then a receipt was added to the thread
const receipt = thread.getReadReceiptForUserId(userId);
expect(receipt?.eventId).toEqual(message.getId());
});
it("Creates a local echo receipt even for events BEFORE an existing receipt", async () => {
// Assumption: no server side support because if we have it, events
// can only be added to the timeline after the thread has been
// initialised, and we are not properly initialising it here.
expect(Thread.hasServerSideSupport).toBe(FeatureSupport.None);
// Given a client WITH relations recursion support
const client = createClientWithEventMapper(
new Map([[Feature.RelationsRecursion, ServerSupport.Stable]]),
);
// And a thread with an added event with a lower timestamp than its other events
const userId = "user1";
const { thread, message } = await createThreadAndEvent(client, 200, 100, userId);
// Then a receipt was added to the thread, because relations
// recursion is available, so we trust the server to have
// provided us with events in the right order.
const receipt = thread.getReadReceiptForUserId(userId);
expect(receipt?.eventId).toEqual(message.getId());
});
});
async function createThreadAndEvent(
client: MatrixClient,
rootTs: number,
eventTs: number,
userId: string,
): Promise<{ thread: Thread; message: MatrixEvent }> {
const room = new Room("room1", client, userId);
// Given a thread
const { thread } = mkThread({
room,
client,
authorId: userId,
participantUserIds: [],
ts: rootTs,
});
// Sanity: the current receipt is for the thread root
expect(thread.getReadReceiptForUserId(userId)?.eventId).toEqual(thread.rootEvent?.getId());
const awaitTimelineEvent = new Promise<void>((res) => thread.on(RoomEvent.Timeline, () => res()));
// When we add a message that is before the latest receipt
const message = makeThreadEvent({
event: true,
rootEventId: thread.id,
replyToEventId: thread.id,
user: userId,
room: room.roomId,
ts: eventTs,
});
await thread.addEvent(message, false, true);
await awaitTimelineEvent;
return { thread, message };
}
function createClientWithEventMapper(canSupport: Map<Feature, ServerSupport> = new Map()): MatrixClient {
const client = mock(MatrixClient, "MatrixClient");
client.reEmitter = mock(ReEmitter, "ReEmitter");
client.canSupport = canSupport;
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
mocked(client.supportsThreads).mockReturnValue(true);
return client;
}
});
describe("Editing events", () => {
describe("Given server support for threads", () => {
let previousThreadHasServerSideSupport: FeatureSupport;
beforeAll(() => {
previousThreadHasServerSideSupport = Thread.hasServerSideSupport;
Thread.hasServerSideSupport = FeatureSupport.Stable;
});
afterAll(() => {
Thread.hasServerSideSupport = previousThreadHasServerSideSupport;
});
it("Adds edits from sync to the thread timeline and applies them", async () => {
// Given a thread
const client = createClient();
const user = "@alice:matrix.org";
const room = "!room:z";
const thread = await createThread(client, user, room);
// When a message and an edit are added to the thread
const messageToEdit = createThreadMessage(thread.id, user, room, "Thread reply");
const editEvent = mkEdit(messageToEdit, client, user, room, "edit");
await thread.addEvent(messageToEdit, false);
await thread.addEvent(editEvent, false);
// Then both events end up in the timeline
const lastEvent = thread.timeline.at(-1)!;
const secondLastEvent = thread.timeline.at(-2)!;
expect(lastEvent).toBe(editEvent);
expect(secondLastEvent).toBe(messageToEdit);
// And the first message has been edited
expect(secondLastEvent.getContent().body).toEqual("edit");
});
it("Adds edits fetched on demand to the thread timeline and applies them", async () => {
// Given we don't support recursive relations
const client = createClient(new Map([[Feature.RelationsRecursion, ServerSupport.Unsupported]]));
// And we have a thread
const user = "@alice:matrix.org";
const room = "!room:z";
const thread = await createThread(client, user, room);
// When a message is added to the thread, and an edit to it is provided on demand
const messageToEdit = createThreadMessage(thread.id, user, room, "Thread reply");
// (fetchEditsWhereNeeded only applies to encrypted messages for some reason)
messageToEdit.event.type = EventType.RoomMessageEncrypted;
const editEvent = mkEdit(messageToEdit, client, user, room, "edit");
mocked(client.relations).mockImplementation(async (_roomId, eventId) => {
if (eventId === messageToEdit.getId()) {
return { events: [editEvent] };
} else {
return { events: [] };
}
});
await thread.addEvent(messageToEdit, false);
// Then both events end up in the timeline
const lastEvent = thread.timeline.at(-1)!;
const secondLastEvent = thread.timeline.at(-2)!;
expect(lastEvent).toBe(editEvent);
expect(secondLastEvent).toBe(messageToEdit);
// And the first message has been edited
expect(secondLastEvent.getContent().body).toEqual("edit");
});
});
});
});
/**
* Create a message event that lives in a thread
*/
function createThreadMessage(threadId: string, user: string, room: string, msg: string): MatrixEvent {
return makeThreadEvent({
event: true,
user,
room,
msg,
rootEventId: threadId,
replyToEventId: threadId,
});
}
/**
* Create a thread and wait for it to be properly initialised (so you can safely
* add events to it and expect them to appear in the timeline.
*/
async function createThread(client: MatrixClient, user: string, roomId: string): Promise<Thread> {
const root = mkMessage({ event: true, user, room: roomId, msg: "Thread root" });
const room = new Room(roomId, client, "@roomcreator:x");
// Ensure the root is in the room timeline
root.setThreadId(root.getId());
await room.addLiveEvents([root]);
// Create the thread and wait for it to be initialised
const thread = room.createThread(root.getId()!, root, [], false);
await new Promise<void>((res) => thread.once(RoomEvent.TimelineReset, () => res()));
return thread;
}
/**
* Create a MatrixClient that supports threads and has all the methods used when
* creating a thread that call out to HTTP endpoints mocked out.
*/
function createClient(canSupport = new Map()): MatrixClient {
const client = mock(MatrixClient, "MatrixClient");
client.reEmitter = mock(ReEmitter, "ReEmitter");
client.canSupport = canSupport;
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
// Mock methods that call out to HTTP endpoints
jest.spyOn(client, "paginateEventTimeline").mockResolvedValue(true);
jest.spyOn(client, "relations").mockResolvedValue({ events: [] });
jest.spyOn(client, "fetchRoomEvent").mockResolvedValue({});
return client;
}
+395
View File
@@ -0,0 +1,395 @@
/**
* @jest-environment jsdom
*/
/*
Copyright 2023 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 fetchMock from "fetch-mock-jest";
import { mocked } from "jest-mock";
import jwtDecode from "jwt-decode";
import { Method } from "../../../src";
import * as crypto from "../../../src/crypto/crypto";
import { logger } from "../../../src/logger";
import {
completeAuthorizationCodeGrant,
generateAuthorizationParams,
generateAuthorizationUrl,
generateOidcAuthorizationUrl,
} from "../../../src/oidc/authorize";
import { OidcError } from "../../../src/oidc/error";
import { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../test-utils/oidc";
jest.mock("jwt-decode");
// save for resetting mocks
const realSubtleCrypto = crypto.subtleCrypto;
describe("oidc authorization", () => {
const delegatedAuthConfig = makeDelegatedAuthConfig();
const authorizationEndpoint = delegatedAuthConfig.metadata.authorization_endpoint;
const tokenEndpoint = delegatedAuthConfig.metadata.token_endpoint;
const clientId = "xyz789";
const baseUrl = "https://test.com";
// 14.03.2022 16:15
const now = 1647270879403;
beforeAll(() => {
jest.spyOn(logger, "warn");
jest.setSystemTime(now);
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
});
afterEach(() => {
// @ts-ignore reset any ugly mocking we did
crypto.subtleCrypto = realSubtleCrypto;
});
it("should generate authorization params", () => {
const result = generateAuthorizationParams({ redirectUri: baseUrl });
expect(result.redirectUri).toEqual(baseUrl);
// random strings
expect(result.state.length).toEqual(8);
expect(result.nonce.length).toEqual(8);
expect(result.codeVerifier.length).toEqual(64);
const expectedScope =
"openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:";
expect(result.scope.startsWith(expectedScope)).toBeTruthy();
// deviceId of 10 characters is appended to the device scope
expect(result.scope.length).toEqual(expectedScope.length + 10);
});
describe("generateAuthorizationUrl()", () => {
it("should generate url with correct parameters", async () => {
// test the no crypto case here
// @ts-ignore mocking
crypto.subtleCrypto = undefined;
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
const authUrl = new URL(
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
);
expect(authUrl.searchParams.get("response_mode")).toEqual("query");
expect(authUrl.searchParams.get("response_type")).toEqual("code");
expect(authUrl.searchParams.get("client_id")).toEqual(clientId);
expect(authUrl.searchParams.get("code_challenge_method")).toEqual("S256");
expect(authUrl.searchParams.get("scope")).toEqual(authorizationParams.scope);
expect(authUrl.searchParams.get("state")).toEqual(authorizationParams.state);
expect(authUrl.searchParams.get("nonce")).toEqual(authorizationParams.nonce);
// crypto not available, plain text code_challenge is used
expect(authUrl.searchParams.get("code_challenge")).toEqual(authorizationParams.codeVerifier);
expect(logger.warn).toHaveBeenCalledWith(
"A secure context is required to generate code challenge. Using plain text code challenge",
);
});
});
describe("generateOidcAuthorizationUrl()", () => {
it("should generate url with correct parameters", async () => {
const nonce = "abc123";
const metadata = delegatedAuthConfig.metadata;
const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
nonce,
}),
);
expect(authUrl.searchParams.get("response_mode")).toEqual("query");
expect(authUrl.searchParams.get("response_type")).toEqual("code");
expect(authUrl.searchParams.get("client_id")).toEqual(clientId);
expect(authUrl.searchParams.get("code_challenge_method")).toEqual("S256");
// scope minus the 10char random device id at the end
expect(authUrl.searchParams.get("scope")!.slice(0, -10)).toEqual(
"openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:",
);
expect(authUrl.searchParams.get("state")).toBeTruthy();
expect(authUrl.searchParams.get("nonce")).toEqual(nonce);
expect(authUrl.searchParams.get("code_challenge")).toBeTruthy();
});
});
describe("completeAuthorizationCodeGrant", () => {
const homeserverUrl = "https://server.org/";
const identityServerUrl = "https://id.org/";
const nonce = "test-nonce";
const redirectUri = baseUrl;
const code = "auth_code_xyz";
const validBearerTokenResponse = {
token_type: "Bearer",
access_token: "test_access_token",
refresh_token: "test_refresh_token",
id_token: "valid.id.token",
expires_in: 300,
};
const metadata = mockOpenIdConfiguration();
const validDecodedIdToken = {
// nonce matches
nonce,
// not expired
exp: Date.now() / 1000 + 100000,
// audience is this client
aud: clientId,
// issuer matches
iss: metadata.issuer,
sub: "123",
};
const mockSessionStorage = (state: Record<string, unknown>): void => {
jest.spyOn(sessionStorage.__proto__, "getItem").mockImplementation((key: unknown) => {
return state[key as string] ?? null;
});
jest.spyOn(sessionStorage.__proto__, "setItem").mockImplementation(
// @ts-ignore mock type
(key: string, value: unknown) => (state[key] = value),
);
jest.spyOn(sessionStorage.__proto__, "removeItem").mockImplementation((key: unknown) => {
const { [key as string]: value, ...newState } = state;
state = newState;
return value;
});
};
const getValueFromStorage = <T = string>(state: string, key: string): T => {
const storedState = window.sessionStorage.getItem(`mx_oidc_${state}`)!;
return JSON.parse(storedState)[key] as unknown as T;
};
/**
* These tests kind of integration test oidc auth, by using `generateOidcAuthorizationUrl` and mocked storage
* to mock the use case of initiating oidc auth, putting state in storage, redirecting to OP,
* then returning and using state to verfiy.
* Returns random state string used to access storage
* @param params
*/
const setupState = async (params = {}): Promise<string> => {
const url = await generateOidcAuthorizationUrl({
metadata,
redirectUri,
clientId,
homeserverUrl,
identityServerUrl,
nonce,
...params,
});
const state = new URL(url).searchParams.get("state")!;
// add the scope with correct deviceId to the mocked bearer token response
const scope = getValueFromStorage(state, "scope");
fetchMock.post(metadata.token_endpoint, {
status: 200,
headers: {
"Content-Type": "application/json",
},
...validBearerTokenResponse,
scope,
});
return state;
};
beforeEach(() => {
fetchMock.mockClear();
fetchMock.resetBehavior();
fetchMock.get(`${metadata.issuer}.well-known/openid-configuration`, metadata);
fetchMock.get(`${metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
mockSessionStorage({});
mocked(jwtDecode).mockReturnValue(validDecodedIdToken);
});
it("should make correct request to the token endpoint", async () => {
const state = await setupState();
const codeVerifier = getValueFromStorage(state, "code_verifier");
await completeAuthorizationCodeGrant(code, state);
expect(fetchMock).toHaveBeenCalledWith(
metadata.token_endpoint,
expect.objectContaining({
method: Method.Post,
credentials: "same-origin",
headers: {
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
}),
);
// check body is correctly formed
const queryParams = fetchMock.mock.calls.find(([endpoint]) => endpoint === metadata.token_endpoint)![1]!
.body as URLSearchParams;
expect(queryParams.get("grant_type")).toEqual("authorization_code");
expect(queryParams.get("client_id")).toEqual(clientId);
expect(queryParams.get("code_verifier")).toEqual(codeVerifier);
expect(queryParams.get("redirect_uri")).toEqual(redirectUri);
expect(queryParams.get("code")).toEqual(code);
});
it("should return with valid bearer token", async () => {
const state = await setupState();
const scope = getValueFromStorage(state, "scope");
const result = await completeAuthorizationCodeGrant(code, state);
expect(result).toEqual({
homeserverUrl,
identityServerUrl,
oidcClientSettings: {
clientId,
issuer: metadata.issuer,
},
tokenResponse: {
access_token: validBearerTokenResponse.access_token,
id_token: validBearerTokenResponse.id_token,
refresh_token: validBearerTokenResponse.refresh_token,
token_type: validBearerTokenResponse.token_type,
// this value is slightly unstable because it uses the clock
expires_at: result.tokenResponse.expires_at,
scope,
},
});
});
it("should return with valid bearer token where token_type is lowercase", async () => {
const state = await setupState();
const scope = getValueFromStorage(state, "scope");
const tokenResponse = {
...validBearerTokenResponse,
scope,
token_type: "bearer",
};
fetchMock.post(
tokenEndpoint,
{
headers: {
"Content-Type": "application/json",
},
...tokenResponse,
},
{ overwriteRoutes: true },
);
const result = await completeAuthorizationCodeGrant(code, state);
expect(result).toEqual({
homeserverUrl,
identityServerUrl,
oidcClientSettings: {
clientId,
issuer: metadata.issuer,
},
// results in token that uses 'Bearer' token type
tokenResponse: {
access_token: validBearerTokenResponse.access_token,
id_token: validBearerTokenResponse.id_token,
refresh_token: validBearerTokenResponse.refresh_token,
token_type: "Bearer",
// this value is slightly unstable because it uses the clock
expires_at: result.tokenResponse.expires_at,
scope,
},
});
expect(result.tokenResponse.token_type).toEqual("Bearer");
});
it("should throw when state is not found in storage", async () => {
// don't setup sessionStorage with expected state
const state = "abc123";
fetchMock.post(
metadata.token_endpoint,
{
status: 500,
},
{ overwriteRoutes: true },
);
await expect(() => completeAuthorizationCodeGrant(code, state)).rejects.toThrow(
new Error(OidcError.MissingOrInvalidStoredState),
);
});
it("should throw with code exchange failed error when request fails", async () => {
const state = await setupState();
fetchMock.post(
metadata.token_endpoint,
{
status: 500,
},
{ overwriteRoutes: true },
);
await expect(() => completeAuthorizationCodeGrant(code, state)).rejects.toThrow(
new Error(OidcError.CodeExchangeFailed),
);
});
it("should throw invalid token error when token is invalid", async () => {
const state = await setupState();
const invalidBearerTokenResponse = {
...validBearerTokenResponse,
access_token: null,
};
fetchMock.post(
metadata.token_endpoint,
{
headers: {
"Content-Type": "application/json",
},
...invalidBearerTokenResponse,
},
{ overwriteRoutes: true },
);
await expect(() => completeAuthorizationCodeGrant(code, state)).rejects.toThrow(
new Error(OidcError.InvalidBearerTokenResponse),
);
});
it("should throw invalid id token error when id_token is invalid", async () => {
const state = await setupState();
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
// invalid audience
aud: "something-else",
});
await expect(() => completeAuthorizationCodeGrant(code, state)).rejects.toThrow(
new Error(OidcError.InvalidIdToken),
);
});
});
});
+84
View File
@@ -0,0 +1,84 @@
/*
Copyright 2023 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 fetchMockJest from "fetch-mock-jest";
import { OidcError } from "../../../src/oidc/error";
import { registerOidcClient } from "../../../src/oidc/register";
describe("registerOidcClient()", () => {
const issuer = "https://auth.com/";
const registrationEndpoint = "https://auth.com/register";
const clientName = "Element";
const baseUrl = "https://just.testing";
const dynamicClientId = "xyz789";
const delegatedAuthConfig = {
issuer,
registrationEndpoint,
authorizationEndpoint: issuer + "auth",
tokenEndpoint: issuer + "token",
};
beforeEach(() => {
fetchMockJest.mockClear();
fetchMockJest.resetBehavior();
});
it("should make correct request to register client", async () => {
fetchMockJest.post(registrationEndpoint, {
status: 200,
body: JSON.stringify({ client_id: dynamicClientId }),
});
expect(await registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).toEqual(dynamicClientId);
expect(fetchMockJest).toHaveBeenCalledWith(registrationEndpoint, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
client_name: clientName,
client_uri: baseUrl,
response_types: ["code"],
grant_types: ["authorization_code", "refresh_token"],
redirect_uris: [baseUrl],
id_token_signed_response_alg: "RS256",
token_endpoint_auth_method: "none",
application_type: "web",
}),
});
});
it("should throw when registration request fails", async () => {
fetchMockJest.post(registrationEndpoint, {
status: 500,
});
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
OidcError.DynamicRegistrationFailed,
);
});
it("should throw when registration response is invalid", async () => {
fetchMockJest.post(registrationEndpoint, {
status: 200,
// no clientId in response
body: "{}",
});
await expect(() => registerOidcClient(delegatedAuthConfig, clientName, baseUrl)).rejects.toThrow(
OidcError.DynamicRegistrationInvalid,
);
});
});
+289
View File
@@ -0,0 +1,289 @@
/*
Copyright 2023 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 jwtDecode from "jwt-decode";
import { M_AUTHENTICATION } from "../../../src";
import { logger } from "../../../src/logger";
import {
validateIdToken,
validateOIDCIssuerWellKnown,
validateWellKnownAuthentication,
} from "../../../src/oidc/validate";
import { OidcError } from "../../../src/oidc/error";
jest.mock("jwt-decode");
describe("validateWellKnownAuthentication()", () => {
const baseWk = {
"m.homeserver": {
base_url: "https://hs.org",
},
};
it("should throw not supported error when wellKnown has no m.authentication section", () => {
expect(() => validateWellKnownAuthentication(undefined)).toThrow(OidcError.NotSupported);
});
it("should throw misconfigured error when authentication issuer is not a string", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: { url: "test.com" },
},
};
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
OidcError.Misconfigured,
);
});
it("should throw misconfigured error when authentication account is not a string", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: { url: "test" },
},
};
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
OidcError.Misconfigured,
);
});
it("should throw misconfigured error when authentication account is false", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: false,
},
};
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
OidcError.Misconfigured,
);
});
it("should return valid config when wk uses stable m.authentication", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: "account.com",
},
};
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
issuer: "test.com",
account: "account.com",
});
});
it("should return valid config when m.authentication account is missing", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
},
};
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
issuer: "test.com",
});
});
it("should remove unexpected properties", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
somethingElse: "test",
},
};
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
issuer: "test.com",
});
});
});
describe("validateOIDCIssuerWellKnown", () => {
const validWk: any = {
authorization_endpoint: "https://test.org/authorize",
token_endpoint: "https://authorize.org/token",
registration_endpoint: "https://authorize.org/regsiter",
revocation_endpoint: "https://authorize.org/regsiter",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256"],
};
beforeEach(() => {
// stub to avoid console litter
jest.spyOn(logger, "error")
.mockClear()
.mockImplementation(() => {});
});
it("should throw OP support error when wellKnown is not an object", () => {
expect(() => {
validateOIDCIssuerWellKnown([]);
}).toThrow(OidcError.OpSupport);
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
});
it("should log all errors before throwing", () => {
expect(() => {
validateOIDCIssuerWellKnown({
...validWk,
authorization_endpoint: undefined,
response_types_supported: [],
});
}).toThrow(OidcError.OpSupport);
expect(logger.error).toHaveBeenCalledWith("Missing or invalid property: authorization_endpoint");
expect(logger.error).toHaveBeenCalledWith("Invalid property: response_types_supported. code is required.");
});
it("should return validated issuer config", () => {
expect(validateOIDCIssuerWellKnown(validWk)).toEqual({
authorizationEndpoint: validWk.authorization_endpoint,
tokenEndpoint: validWk.token_endpoint,
registrationEndpoint: validWk.registration_endpoint,
});
});
it("should return validated issuer config without registrationendpoint", () => {
const wk = { ...validWk };
delete wk.registration_endpoint;
expect(validateOIDCIssuerWellKnown(wk)).toEqual({
authorizationEndpoint: validWk.authorization_endpoint,
tokenEndpoint: validWk.token_endpoint,
registrationEndpoint: undefined,
});
});
type TestCase = [string, any];
it.each<TestCase>([
["authorization_endpoint", undefined],
["authorization_endpoint", { not: "a string" }],
["token_endpoint", undefined],
["token_endpoint", { not: "a string" }],
["registration_endpoint", { not: "a string" }],
["response_types_supported", undefined],
["response_types_supported", "not an array"],
["response_types_supported", ["doesnt include code"]],
["grant_types_supported", undefined],
["grant_types_supported", "not an array"],
["grant_types_supported", ["doesnt include authorization_code"]],
["code_challenge_methods_supported", undefined],
["code_challenge_methods_supported", "not an array"],
["code_challenge_methods_supported", ["doesnt include S256"]],
])("should throw OP support error when %s is %s", (key, value) => {
const wk = {
...validWk,
[key]: value,
};
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcError.OpSupport);
});
});
describe("validateIdToken()", () => {
const nonce = "test-nonce";
const issuer = "https://auth.org/issuer";
const clientId = "test-client-id";
const idToken = "test-id-token";
const validDecodedIdToken = {
// nonce matches
nonce,
// not expired
exp: Date.now() / 1000 + 5555,
// audience is this client
aud: clientId,
// issuer matches
iss: issuer,
};
beforeEach(() => {
mocked(jwtDecode).mockClear().mockReturnValue(validDecodedIdToken);
jest.spyOn(logger, "error").mockClear();
});
it("should throw when idToken is falsy", () => {
expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
});
it("should throw when idToken cannot be decoded", () => {
mocked(jwtDecode).mockImplementation(() => {
throw new Error("oh no!");
});
expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
});
it("should throw when issuer does not match", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
iss: "https://badissuer.com",
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid issuer"));
});
it("should throw when audience does not include clientId", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
aud: "qwerty,uiop,asdf",
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
});
it("should throw when audience includes clientId and other audiences", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
aud: `${clientId},uiop,asdf`,
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
});
it("should throw when nonce does not match", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
nonce: "something else",
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid nonce"));
});
it("should throw when token does not have an expiry", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
exp: undefined,
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry"));
});
it("should throw when token is expired", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
// expired in the past
exp: Date.now() / 1000 - 777,
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry"));
});
it("should not throw for a valid id token", () => {
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).not.toThrow();
});
});
+1 -79
View File
@@ -97,51 +97,6 @@ describe("NotificationService", function () {
pattern: "foo*bar",
rule_id: "foobar",
},
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
enabled: true,
pattern: "p[io]ng",
rule_id: "pingpong",
},
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
enabled: true,
pattern: "I ate [0-9] pies",
rule_id: "pies",
},
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
enabled: true,
pattern: "b[!ai]ke",
rule_id: "bakebike",
},
],
override: [
{
@@ -289,39 +244,6 @@ describe("NotificationService", function () {
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour.
//
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1 which
// describes pattern should glob:
//
// 1. * matches 0 or more characters;
// 2. ? matches exactly one character
it("should bing on character group ([abc]) bing words.", function () {
testEvent.event.content!.body = "Ping!";
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content!.body = "Pong!";
actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour. (See above.)
it("should bing on character range ([a-z]) bing words.", function () {
testEvent.event.content!.body = "I ate 6 pies";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour. (See above.)
it("should bing on character negation ([!a]) bing words.", function () {
testEvent.event.content!.body = "boke";
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content!.body = "bake";
actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
it("should not bing on room server ACL changes", function () {
testEvent = utils.mkEvent({
type: EventType.RoomServerAcl,
@@ -735,7 +657,7 @@ describe("NotificationService", function () {
content: {
"body": "",
"msgtype": "m.text",
"org.matrix.msc3952.mentions": {},
"m.mentions": {},
},
});
+3 -3
View File
@@ -195,7 +195,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
it("retries on retryImmediately()", async function () {
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
@@ -219,7 +219,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
it("retries on when client is started", async function () {
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
await Promise.all([client.startClient(), httpBackend.flush("/_matrix/client/versions", 1, 20)]);
@@ -243,7 +243,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
it("retries when a message is retried", async function () {
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
versions: ["r0.0.1"],
versions: ["v1.1"],
});
await Promise.all([client.startClient(), httpBackend.flush(undefined, 1, 20)]);
+130 -32
View File
@@ -16,9 +16,9 @@ limitations under the License.
import MockHttpBackend from "matrix-mock-request";
import { MAIN_ROOM_TIMELINE, ReceiptType } from "../../src/@types/read_receipts";
import { MAIN_ROOM_TIMELINE, ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts";
import { MatrixClient } from "../../src/client";
import { EventType } from "../../src/matrix";
import { EventType, MatrixEvent, Room } from "../../src/matrix";
import { synthesizeReceipt } from "../../src/models/read-receipt";
import { encodeUri } from "../../src/utils";
import * as utils from "../test-utils/test-utils";
@@ -42,42 +42,46 @@ let httpBackend: MockHttpBackend;
const THREAD_ID = "$thread_event_id";
const ROOM_ID = "!123:matrix.org";
const threadEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a thread",
"m.relates_to": {
"event_id": THREAD_ID,
"m.in_reply_to": {
event_id: THREAD_ID,
},
"rel_type": "m.thread",
},
},
});
const roomEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
body: "Hello from a room",
},
});
describe("Read receipt", () => {
let threadEvent: MatrixEvent;
let roomEvent: MatrixEvent;
beforeEach(() => {
httpBackend = new MockHttpBackend();
client = new MatrixClient({
userId: "@user:server",
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
client.isGuest = () => false;
client.supportsThreads = () => true;
threadEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a thread",
"m.relates_to": {
"event_id": THREAD_ID,
"m.in_reply_to": {
event_id: THREAD_ID,
},
"rel_type": "m.thread",
},
},
});
roomEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
body: "Hello from a room",
},
});
});
describe("sendReceipt", () => {
@@ -143,13 +147,69 @@ describe("Read receipt", () => {
await httpBackend.flushAllExpected();
await flushPromises();
});
it("should send a main timeline read receipt for a reaction to a thread root", async () => {
roomEvent.event.event_id = THREAD_ID;
const reaction = utils.mkReaction(roomEvent, client, client.getSafeUserId(), ROOM_ID);
const thread = new Room(ROOM_ID, client, client.getSafeUserId()).createThread(
THREAD_ID,
roomEvent,
[threadEvent],
false,
);
threadEvent.setThread(thread);
reaction.setThread(thread);
httpBackend
.when(
"POST",
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: reaction.getId()!,
}),
)
.check((request) => {
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
})
.respond(200, {});
client.sendReceipt(reaction, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("should always send unthreaded receipts if threads support is disabled", async () => {
client.supportsThreads = () => false;
httpBackend
.when(
"POST",
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: roomEvent.getId()!,
}),
)
.check((request) => {
expect(request.data.thread_id).toEqual(undefined);
})
.respond(200, {});
client.sendReceipt(roomEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
});
describe("synthesizeReceipt", () => {
it.each([
{ event: roomEvent, destinationId: MAIN_ROOM_TIMELINE },
{ event: threadEvent, destinationId: threadEvent.threadRootId! },
])("adds the receipt to $destinationId", ({ event, destinationId }) => {
{ getEvent: () => roomEvent, destinationId: MAIN_ROOM_TIMELINE },
{ getEvent: () => threadEvent, destinationId: THREAD_ID },
])("adds the receipt to $destinationId", ({ getEvent, destinationId }) => {
const event = getEvent();
const userId = "@bob:example.org";
const receiptType = ReceiptType.Read;
@@ -160,4 +220,42 @@ describe("Read receipt", () => {
expect(content.thread_id).toEqual(destinationId);
});
});
describe("addReceiptToStructure", () => {
it("should not allow an older unthreaded receipt to clobber a `main` threaded one", () => {
const userId = client.getSafeUserId();
const room = new Room(ROOM_ID, client, userId);
const unthreadedReceipt: WrappedReceipt = {
eventId: "$olderEvent",
data: {
ts: 1234567880,
},
};
const mainTimelineReceipt: WrappedReceipt = {
eventId: "$newerEvent",
data: {
ts: 1234567890,
},
};
room.addReceiptToStructure(
mainTimelineReceipt.eventId,
ReceiptType.ReadPrivate,
userId,
mainTimelineReceipt.data,
false,
);
expect(room.getEventReadUpTo(userId)).toBe(mainTimelineReceipt.eventId);
room.addReceiptToStructure(
unthreadedReceipt.eventId,
ReceiptType.ReadPrivate,
userId,
unthreadedReceipt.data,
false,
);
expect(room.getEventReadUpTo(userId)).toBe(mainTimelineReceipt.eventId);
});
});
});
+6 -7
View File
@@ -96,7 +96,7 @@ describe("ECDHv2", function () {
expect(aliceChecksum).toEqual(bobChecksum);
expect(alice.connect()).rejects.toThrow();
await expect(alice.connect()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
@@ -120,9 +120,9 @@ describe("ECDHv2", function () {
alice.close();
expect(alice.connect()).rejects.toThrow();
expect(alice.send({})).rejects.toThrow();
expect(alice.receive()).rejects.toThrow();
await expect(alice.connect()).rejects.toThrow();
await expect(alice.send({})).rejects.toThrow();
await expect(alice.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
@@ -146,10 +146,10 @@ describe("ECDHv2", function () {
// send a message without encryption
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
expect(bob.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
await expect(bob.receive()).rejects.toThrow();
});
it("ciphertext before set up", async function () {
@@ -164,9 +164,8 @@ describe("ECDHv2", function () {
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
expect(alice.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await expect(alice.receive()).rejects.toThrow();
});
});
});
+4 -4
View File
@@ -172,7 +172,7 @@ describe("Rendezvous", function () {
const cancelPromise = aliceRz.cancel(RendezvousFailureReason.UserDeclined);
await httpBackend.flush("");
expect(cancelPromise).resolves.toBeUndefined();
await expect(cancelPromise).resolves.toBeUndefined();
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequests();
@@ -603,7 +603,7 @@ describe("Rendezvous", function () {
it("device not online within timeout", async function () {
const { aliceRz } = await completeLogin({});
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("device appears online within timeout", async function () {
@@ -627,7 +627,7 @@ describe("Rendezvous", function () {
getFingerprint: () => "bbbb",
};
}, 1500);
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("mismatched device key", async function () {
@@ -636,6 +636,6 @@ describe("Rendezvous", function () {
getFingerprint: () => "XXXX",
},
});
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
await expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
});
});
@@ -95,10 +95,10 @@ describe("SimpleHttpRendezvousTransport", function () {
httpBackend.verifyNoOutstandingExpectation();
}
}
it("should throw an error when no server available", function () {
it("should throw an error when no server available", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
await expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
});
it("POST to fallback server", async function () {
@@ -130,7 +130,6 @@ describe("SimpleHttpRendezvousTransport", function () {
fetchFn,
});
const prom = simpleHttpTransport.send({});
expect(prom).rejects.toThrow();
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
@@ -138,7 +137,7 @@ describe("SimpleHttpRendezvousTransport", function () {
headers: {},
},
};
await httpBackend.flush("");
await Promise.all([expect(prom).rejects.toThrow(), httpBackend.flush("")]);
});
it("POST with absolute path response", async function () {
@@ -364,7 +363,7 @@ describe("SimpleHttpRendezvousTransport", function () {
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
expect(simpleHttpTransport.details()).rejects.toThrow();
await expect(simpleHttpTransport.details()).rejects.toThrow();
});
it("send after cancelled", async function () {
@@ -375,7 +374,7 @@ describe("SimpleHttpRendezvousTransport", function () {
fetchFn,
});
await simpleHttpTransport.cancel(RendezvousFailureReason.UserDeclined);
expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
await expect(simpleHttpTransport.send({})).resolves.toBeUndefined();
});
it("receive before ready", async function () {
@@ -385,7 +384,7 @@ describe("SimpleHttpRendezvousTransport", function () {
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
expect(simpleHttpTransport.receive()).rejects.toThrow();
await expect(simpleHttpTransport.receive()).rejects.toThrow();
});
it("404 failure callback", async function () {
@@ -398,7 +397,6 @@ describe("SimpleHttpRendezvousTransport", function () {
onFailure,
});
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined();
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
@@ -406,7 +404,10 @@ describe("SimpleHttpRendezvousTransport", function () {
headers: {},
},
};
await httpBackend.flush("", 1);
await Promise.all([
expect(simpleHttpTransport.send({ foo: "baa" })).resolves.toBeUndefined(),
httpBackend.flush("", 1),
]);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Unknown);
});
@@ -438,7 +439,6 @@ describe("SimpleHttpRendezvousTransport", function () {
}
{
// GET with 404 to simulate expiry
expect(simpleHttpTransport.receive()).resolves.toBeUndefined();
httpBackend.when("GET", "https://fallbackserver/rz/123").response = {
body: { foo: "baa" },
response: {
@@ -446,7 +446,7 @@ describe("SimpleHttpRendezvousTransport", function () {
headers: {},
},
};
await httpBackend.flush("");
await Promise.all([expect(simpleHttpTransport.receive()).resolves.toBeUndefined(), httpBackend.flush("")]);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired);
}
});
+76
View File
@@ -0,0 +1,76 @@
/*
Copyright 2023 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 fetchMock from "fetch-mock-jest";
import { EventType, MatrixClient, Room } from "../../src";
import { RoomHierarchy } from "../../src/room-hierarchy";
describe("RoomHierarchy", () => {
const roomId = "!room:server";
const client = new MatrixClient({ baseUrl: "https://server", userId: "@user:server" });
it("should load data from /hierarchy API", async () => {
const spy = fetchMock.getOnce(
`https://server/_matrix/client/v1/rooms/${encodeURIComponent(roomId)}/hierarchy?suggested_only=false`,
{
rooms: [],
},
{ overwriteRoutes: true },
);
const room = new Room(roomId, client, client.getSafeUserId());
const hierarchy = new RoomHierarchy(room);
const res = await hierarchy.load();
expect(spy).toHaveBeenCalled();
expect(res).toHaveLength(0);
});
describe("itSuggested", () => {
it("should return true if a room is suggested", async () => {
const spy = fetchMock.getOnce(
`https://server/_matrix/client/v1/rooms/${encodeURIComponent(roomId)}/hierarchy?suggested_only=false`,
{
rooms: [
{
children_state: [
{
origin_server_ts: 111,
content: {
suggested: true,
via: ["matrix.org"],
},
type: EventType.SpaceChild,
state_key: "!child_room:server",
},
],
room_id: roomId,
},
],
},
{ overwriteRoutes: true },
);
const room = new Room(roomId, client, client.getSafeUserId());
const hierarchy = new RoomHierarchy(room);
await hierarchy.load();
expect(spy).toHaveBeenCalled();
expect(hierarchy.isSuggested(hierarchy.root.roomId, "!child_room:server")).toBeTruthy();
});
});
});
+133 -1
View File
@@ -14,9 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import * as utils from "../test-utils/test-utils";
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
import { EventType, RoomState } from "../../src";
import {
createClient,
EventType,
MatrixClient,
RoomState,
UNSTABLE_MSC2666_MUTUAL_ROOMS,
UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS,
UNSTABLE_MSC2666_SHARED_ROOMS,
} from "../../src";
describe("RoomMember", function () {
const roomId = "!foo:bar";
@@ -481,3 +491,125 @@ describe("RoomMember", function () {
});
});
});
describe("MutualRooms", () => {
let client: MatrixClient;
const HS_URL = "https://example.com";
const TEST_USER_ID = "@alice:localhost";
const TEST_DEVICE_ID = "xzcvb";
const QUERIED_USER = "@user:example.com";
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = true;
client = createClient({
baseUrl: HS_URL,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
});
});
afterEach(async () => {
await client.stopClient();
fetchMock.mockReset();
});
function enableFeature(feature: string) {
const mapping: Record<string, boolean> = {};
mapping[feature] = true;
fetchMock.get(`${HS_URL}/_matrix/client/versions`, {
unstable_features: mapping,
versions: ["v1.1"],
});
}
it("supports the initial MSC version (shared rooms)", async () => {
enableFeature(UNSTABLE_MSC2666_SHARED_ROOMS);
fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/shared_rooms/:user_id", (rawUrl) => {
const segments = rawUrl.split("/");
const lastSegment = decodeURIComponent(segments[segments.length - 1]);
expect(lastSegment).toEqual(QUERIED_USER);
return {
joined: ["!test:example.com"],
};
});
const rooms = await client._unstable_getSharedRooms(QUERIED_USER);
expect(rooms).toEqual(["!test:example.com"]);
});
it("supports the renaming MSC version (mutual rooms)", async () => {
enableFeature(UNSTABLE_MSC2666_MUTUAL_ROOMS);
fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms/:user_id", (rawUrl) => {
const segments = rawUrl.split("/");
const lastSegment = decodeURIComponent(segments[segments.length - 1]);
expect(lastSegment).toEqual(QUERIED_USER);
return {
joined: ["!test2:example.com"],
};
});
const rooms = await client._unstable_getSharedRooms(QUERIED_USER);
expect(rooms).toEqual(["!test2:example.com"]);
});
describe("can work the latest MSC version (query mutual rooms)", () => {
beforeEach(() => {
enableFeature(UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS);
});
it("works with a simple response", async () => {
fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms", (rawUrl) => {
const url = new URL(rawUrl);
expect(url.searchParams.get("user_id")).toEqual(QUERIED_USER);
return {
joined: ["!test3:example.com"],
};
});
const rooms = await client._unstable_getSharedRooms(QUERIED_USER);
expect(rooms).toEqual(["!test3:example.com"]);
});
it("works with a paginated response", async () => {
fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms", (rawUrl) => {
const url = new URL(rawUrl);
expect(url.searchParams.get("user_id")).toEqual(QUERIED_USER);
const token = url.searchParams.get("batch_token");
if (token == "yahaha") {
return {
joined: ["!korok:example.com"],
};
} else {
return {
joined: ["!rock:example.com"],
next_batch_token: "yahaha",
};
}
});
const rooms = await client._unstable_getSharedRooms(QUERIED_USER);
expect(rooms).toEqual(["!rock:example.com", "!korok:example.com"]);
});
});
});
+1 -3
View File
@@ -70,9 +70,7 @@ describe("RoomState", function () {
user: userA,
room: roomId,
event: true,
content: {
creator: userA,
},
content: {},
}),
]);
});
+482 -299
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
/*
Copyright 2023 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 * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { CrossSigningIdentity } from "../../../src/rust-crypto/CrossSigningIdentity";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
describe("CrossSigningIdentity", () => {
describe("bootstrapCrossSigning", () => {
/** the CrossSigningIdentity implementation under test */
let crossSigning: CrossSigningIdentity;
/** a mocked-up OlmMachine which crossSigning is connected to */
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
/** A mock OutgoingRequestProcessor which crossSigning is connected to */
let outgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
/** A mock ServerSideSecretStorage which crossSigning is connected to */
let secretStorage: Mocked<ServerSideSecretStorage>;
beforeEach(async () => {
await RustSdkCryptoJs.initAsync();
olmMachine = {
crossSigningStatus: jest.fn(),
bootstrapCrossSigning: jest.fn(),
close: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
outgoingRequestProcessor = {
makeOutgoingRequest: jest.fn(),
} as unknown as Mocked<OutgoingRequestProcessor>;
secretStorage = {
get: jest.fn(),
} as unknown as Mocked<ServerSideSecretStorage>;
crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor, secretStorage);
});
it("should do nothing if keys are present on-device and in secret storage", async () => {
olmMachine.crossSigningStatus.mockResolvedValue({
hasMaster: true,
hasSelfSigning: true,
hasUserSigning: true,
});
// TODO: secret storage
await crossSigning.bootstrapCrossSigning({});
expect(olmMachine.bootstrapCrossSigning).not.toHaveBeenCalled();
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
});
it("should call bootstrapCrossSigning if a reset is forced", async () => {
olmMachine.bootstrapCrossSigning.mockResolvedValue([]);
await crossSigning.bootstrapCrossSigning({ setupNewCrossSigning: true });
expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true);
});
it("should call bootstrapCrossSigning if we need new keys", async () => {
olmMachine.crossSigningStatus.mockResolvedValue({
hasMaster: false,
hasSelfSigning: false,
hasUserSigning: false,
});
olmMachine.bootstrapCrossSigning.mockResolvedValue([]);
await crossSigning.bootstrapCrossSigning({});
expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true);
});
});
});
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import fetchMock from "fetch-mock-jest";
import { Mocked } from "jest-mock";
import { KeysClaimRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js";
import { KeysClaimRequest, UserId } from "@matrix-org/matrix-sdk-crypto-wasm";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
@@ -16,7 +16,7 @@ limitations under the License.
import MockHttpBackend from "matrix-mock-request";
import { Mocked } from "jest-mock";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import {
KeysBackupRequest,
KeysClaimRequest,
@@ -26,11 +26,12 @@ import {
SignatureUploadRequest,
SigningKeysUploadRequest,
ToDeviceRequest,
} from "@matrix-org/matrix-sdk-crypto-js";
} from "@matrix-org/matrix-sdk-crypto-wasm";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, UIAuthCallback } from "../../../src";
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi, UIAuthCallback } from "../../../src";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { defer } from "../../../src/utils";
describe("OutgoingRequestProcessor", () => {
/** the OutgoingRequestProcessor implementation under test */
@@ -161,7 +162,7 @@ describe("OutgoingRequestProcessor", () => {
.when("PUT", "/_matrix")
.check((req) => {
expect(req.path).toEqual(
"https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid",
"https://example.com/_matrix/client/v3/rooms/test%2Froom/send/test%2Ftype/test%2Ftxnid",
);
expect(req.rawData).toEqual(testBody);
expect(req.headers["Accept"]).toEqual("application/json");
@@ -218,4 +219,40 @@ describe("OutgoingRequestProcessor", () => {
await Promise.all([processor.makeOutgoingRequest(outgoingRequest), markSentCallPromise]);
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
});
it("does not explode if the OlmMachine is stopped while the request is in flight", async () => {
// we use a real olm machine for this test
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(
new RustSdkCryptoJs.UserId("@alice:example.com"),
new RustSdkCryptoJs.DeviceId("TEST_DEVICE"),
);
const authRequestResultDefer = defer<string>();
const authRequestCalledPromise = new Promise<void>((resolve) => {
const mockHttpApi = {
authedRequest: async () => {
resolve();
return await authRequestResultDefer.promise;
},
} as unknown as Mocked<MatrixHttpApi<IHttpOpts & { onlyData: true }>>;
processor = new OutgoingRequestProcessor(olmMachine, mockHttpApi);
});
// build a request
const request = olmMachine.queryKeysForUsers([new RustSdkCryptoJs.UserId("@bob:example.com")]);
const result = processor.makeOutgoingRequest(request);
// wait for the HTTP request to be made
await authRequestCalledPromise;
// while the HTTP request is in flight, the OlmMachine gets stopped.
olmMachine.close();
// the HTTP request completes...
authRequestResultDefer.resolve("{}");
// ... and `makeOutgoingRequest` resolves satisfactorily
await result;
});
});
+515 -26
View File
@@ -14,42 +14,81 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-js";
import { Mocked } from "jest-mock";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-wasm";
import { mocked, Mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import { IHttpOpts, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
import {
CryptoEvent,
Device,
DeviceVerification,
HttpApiEvent,
HttpApiEventHandlerMap,
IHttpOpts,
IToDeviceEvent,
MatrixClient,
MatrixEvent,
MatrixHttpApi,
TypedEventEmitter,
} from "../../../src";
import { mkEvent } from "../../test-utils/test-utils";
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "../../../src/@types/crypto";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
import {
CryptoCallbacks,
EventShieldColour,
EventShieldReason,
ImportRoomKeysOpts,
VerificationRequest,
} from "../../../src/crypto-api";
import * as testData from "../../test-utils/test-data";
import { defer } from "../../../src/utils";
const TEST_USER = "@alice:example.com";
const TEST_DEVICE_ID = "TEST_DEVICE";
afterEach(() => {
fetchMock.reset();
});
describe("RustCrypto", () => {
describe(".exportRoomKeys", () => {
describe(".importRoomKeys and .exportRoomKeys", () => {
let rustCrypto: RustCrypto;
beforeEach(async () => {
rustCrypto = await makeTestRustCrypto();
});
beforeEach(
async () => {
rustCrypto = await makeTestRustCrypto();
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
);
it("should import and export keys", async () => {
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
let importTotal = 0;
const opt: ImportRoomKeysOpts = {
progressCallback: (stage) => {
importTotal = stage.total;
},
};
await rustCrypto.importRoomKeys(someRoomKeys, opt);
expect(importTotal).toBe(someRoomKeys.length);
it("should return a list", async () => {
const keys = await rustCrypto.exportRoomKeys();
expect(Array.isArray(keys)).toBeTruthy();
expect(keys.length).toBe(someRoomKeys.length);
const aSession = someRoomKeys[0];
const exportedKey = keys.find((k) => k.session_id == aSession.session_id);
expect(aSession).toStrictEqual(exportedKey);
});
});
@@ -91,21 +130,81 @@ describe("RustCrypto", () => {
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
expect(res).toEqual(inputs);
});
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
const toDeviceEvent = {
type: "m.key.verification.request",
content: {
from_device: "testDeviceId",
methods: ["m.sas.v1"],
transaction_id: "testTxn",
timestamp: Date.now() - 1000,
},
sender: "@user:id",
};
const onEvent = jest.fn();
rustCrypto.on(CryptoEvent.VerificationRequestReceived, onEvent);
await rustCrypto.preprocessToDeviceMessages([toDeviceEvent]);
expect(onEvent).toHaveBeenCalledTimes(1);
const [req]: [VerificationRequest] = onEvent.mock.lastCall;
expect(req.transactionId).toEqual("testTxn");
});
});
it("isCrossSigningReady", async () => {
const rustCrypto = await makeTestRustCrypto();
await expect(rustCrypto.isCrossSigningReady()).resolves.toBe(false);
});
it("getCrossSigningKeyId", async () => {
it("getCrossSigningKeyId when there is no cross signing keys", async () => {
const rustCrypto = await makeTestRustCrypto();
await expect(rustCrypto.getCrossSigningKeyId()).resolves.toBe(null);
});
it("bootstrapCrossSigning", async () => {
describe("getCrossSigningStatus", () => {
it("returns sensible values on a default client", async () => {
const secretStorage = {
isStored: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
const result = await rustCrypto.getCrossSigningStatus();
expect(secretStorage.isStored).toHaveBeenCalledWith("m.cross_signing.master");
expect(result).toEqual({
privateKeysCachedLocally: {
masterKey: false,
selfSigningKey: false,
userSigningKey: false,
},
privateKeysInSecretStorage: false,
publicKeysOnDevice: false,
});
});
it("throws if `stop` is called mid-call", async () => {
const secretStorage = {
isStored: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
// start the call off
const result = rustCrypto.getCrossSigningStatus();
// call `.stop`
rustCrypto.stop();
// getCrossSigningStatus should abort
await expect(result).rejects.toEqual(new Error("MatrixClient has been stopped"));
});
});
it("bootstrapCrossSigning delegates to CrossSigningIdentity", async () => {
const rustCrypto = await makeTestRustCrypto();
const mockCrossSigningIdentity = {
bootstrapCrossSigning: jest.fn().mockResolvedValue(undefined),
};
// @ts-ignore private property
rustCrypto.crossSigningIdentity = mockCrossSigningIdentity;
await rustCrypto.bootstrapCrossSigning({});
expect(mockCrossSigningIdentity.bootstrapCrossSigning).toHaveBeenCalledWith({});
});
it("isSecretStorageReady", async () => {
@@ -166,6 +265,7 @@ describe("RustCrypto", () => {
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
});
@@ -182,6 +282,31 @@ describe("RustCrypto", () => {
expect(outgoingRequestProcessor.makeOutgoingRequest).toHaveBeenCalledWith(testReq);
});
it("should go round the loop again if another sync completes while the first `outgoingRequests` is running", async () => {
// the first call to `outgoingMessages` will return a promise which blocks for a while
const firstOutgoingRequestsDefer = defer<Array<any>>();
mocked(olmMachine.outgoingRequests).mockReturnValueOnce(firstOutgoingRequestsDefer.promise);
// the second will return a KeysQueryRequest.
const testReq = new KeysQueryRequest("1234", "{}");
outgoingRequestQueue.push([testReq]);
// the first sync completes, triggering the first call to `outgoingMessages`
rustCrypto.onSyncCompleted({});
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
// a second /sync completes before the first call to `outgoingRequests` completes. It shouldn't trigger
// a second call immediately, but should queue one up.
rustCrypto.onSyncCompleted({});
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
// the first call now completes, *with an empty result*, which would normally cause us to exit the loop, but
// we should have a second call queued. It should trigger a call to `makeOutgoingRequest`.
firstOutgoingRequestsDefer.resolve([]);
await awaitCallToMakeOutgoingRequest();
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(2);
});
it("stops looping when stop() is called", async () => {
for (let i = 0; i < 5; i++) {
outgoingRequestQueue.push([new KeysQueryRequest("1234", "{}")]);
@@ -255,6 +380,111 @@ describe("RustCrypto", () => {
});
});
describe(".getEncryptionInfoForEvent", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(() => {
olmMachine = {
getRoomEventEncryptionInfo: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
olmMachine,
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
afterEach(() => {
jest.restoreAllMocks();
});
async function makeEncryptedEvent(): Promise<MatrixEvent> {
const encryptedEvent = mkEvent({
event: true,
type: "m.room.encrypted",
content: { algorithm: "fake_alg" },
room: "!room:id",
});
encryptedEvent.event.event_id = "$event:id";
const mockCryptoBackend = {
decryptEvent: () =>
({
clearEvent: { content: { body: "1234" } },
} as unknown as IEventDecryptionResult),
} as unknown as CryptoBackend;
await encryptedEvent.attemptDecryption(mockCryptoBackend);
return encryptedEvent;
}
it("should handle unencrypted events", async () => {
const event = mkEvent({ event: true, type: "m.room.message", content: { body: "xyz" } });
const res = await rustCrypto.getEncryptionInfoForEvent(event);
expect(res).toBe(null);
expect(olmMachine.getRoomEventEncryptionInfo).not.toHaveBeenCalled();
});
it("passes the event into the OlmMachine", async () => {
const encryptedEvent = await makeEncryptedEvent();
const res = await rustCrypto.getEncryptionInfoForEvent(encryptedEvent);
expect(res).toBe(null);
expect(olmMachine.getRoomEventEncryptionInfo).toHaveBeenCalledTimes(1);
const [passedEvent, passedRoom] = olmMachine.getRoomEventEncryptionInfo.mock.calls[0];
expect(passedRoom.toString()).toEqual("!room:id");
expect(JSON.parse(passedEvent)).toStrictEqual(
expect.objectContaining({
event_id: "$event:id",
}),
);
});
it.each([
[RustSdkCryptoJs.ShieldColor.None, EventShieldColour.NONE],
[RustSdkCryptoJs.ShieldColor.Grey, EventShieldColour.GREY],
[RustSdkCryptoJs.ShieldColor.Red, EventShieldColour.RED],
])("gets the right shield color (%i)", async (rustShield, expectedShield) => {
const mockEncryptionInfo = {
shieldState: jest.fn().mockReturnValue({ color: rustShield, message: null }),
} as unknown as RustSdkCryptoJs.EncryptionInfo;
olmMachine.getRoomEventEncryptionInfo.mockResolvedValue(mockEncryptionInfo);
const res = await rustCrypto.getEncryptionInfoForEvent(await makeEncryptedEvent());
expect(mockEncryptionInfo.shieldState).toHaveBeenCalledWith(false);
expect(res).not.toBe(null);
expect(res!.shieldColour).toEqual(expectedShield);
});
it.each([
[null, null],
["Encrypted by an unverified user.", EventShieldReason.UNVERIFIED_IDENTITY],
["Encrypted by a device not verified by its owner.", EventShieldReason.UNSIGNED_DEVICE],
[
"The authenticity of this encrypted message can't be guaranteed on this device.",
EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
],
["Encrypted by an unknown or deleted device.", EventShieldReason.UNKNOWN_DEVICE],
["bloop", EventShieldReason.UNKNOWN],
])("gets the right shield reason (%s)", async (rustReason, expectedReason) => {
// suppress the warning from the unknown shield reason
jest.spyOn(console, "warn").mockImplementation(() => {});
const mockEncryptionInfo = {
shieldState: jest
.fn()
.mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, message: rustReason }),
} as unknown as RustSdkCryptoJs.EncryptionInfo;
olmMachine.getRoomEventEncryptionInfo.mockResolvedValue(mockEncryptionInfo);
const res = await rustCrypto.getEncryptionInfoForEvent(await makeEncryptedEvent());
expect(mockEncryptionInfo.shieldState).toHaveBeenCalledWith(false);
expect(res).not.toBe(null);
expect(res!.shieldReason).toEqual(expectedReason);
});
});
describe("get|setTrustCrossSignedDevices", () => {
let rustCrypto: RustCrypto;
@@ -274,6 +504,60 @@ describe("RustCrypto", () => {
});
});
describe("setDeviceVerified", () => {
let rustCrypto: RustCrypto;
async function getTestDevice(): Promise<Device> {
const devices = await rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
return devices.get(testData.TEST_USER_ID)!.get(testData.TEST_DEVICE_ID)!;
}
beforeEach(async () => {
rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
}),
testData.TEST_USER_ID,
);
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: {
[testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA,
},
},
});
// call onSyncCompleted to kick off the outgoingRequestLoop and download the device list.
rustCrypto.onSyncCompleted({});
// before the call, the device should be unverified.
const device = await getTestDevice();
expect(device.verified).toEqual(DeviceVerification.Unverified);
});
it("should throw an error for an unknown device", async () => {
await expect(rustCrypto.setDeviceVerified(testData.TEST_USER_ID, "xxy")).rejects.toThrow("Unknown device");
});
it("should mark an unverified device as verified", async () => {
await rustCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
// and confirm that the device is now verified
expect((await getTestDevice()).verified).toEqual(DeviceVerification.Verified);
});
it("should mark a verified device as unverified", async () => {
await rustCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
expect((await getTestDevice()).verified).toEqual(DeviceVerification.Verified);
await rustCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID, false);
expect((await getTestDevice()).verified).toEqual(DeviceVerification.Unverified);
});
});
describe("getDeviceVerificationStatus", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
@@ -288,6 +572,7 @@ describe("RustCrypto", () => {
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
@@ -311,6 +596,209 @@ describe("RustCrypto", () => {
expect(res).toBe(null);
});
});
describe("userHasCrossSigningKeys", () => {
let rustCrypto: RustCrypto;
beforeEach(async () => {
rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
}),
testData.TEST_USER_ID,
);
});
it("throws an error if the fetch fails", async () => {
fetchMock.post("path:/_matrix/client/v3/keys/query", 400);
await expect(rustCrypto.userHasCrossSigningKeys()).rejects.toThrow("400 error");
});
it("returns false if the user has no cross-signing keys", async () => {
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: { [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA },
},
});
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(false);
});
it("returns true if the user has cross-signing keys", async () => {
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: { [testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA },
},
...testData.SIGNED_CROSS_SIGNING_KEYS_DATA,
});
await expect(rustCrypto.userHasCrossSigningKeys()).resolves.toBe(true);
});
it("returns true if the user is untracked, downloadUncached is set at true and the cross-signing keys are available", async () => {
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.BOB_TEST_USER_ID]: {
[testData.BOB_TEST_DEVICE_ID]: testData.BOB_SIGNED_TEST_DEVICE_DATA,
},
},
...testData.BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
});
await expect(rustCrypto.userHasCrossSigningKeys(testData.BOB_TEST_USER_ID, true)).resolves.toBe(true);
});
it("returns false if the user is unknown", async () => {
await expect(rustCrypto.userHasCrossSigningKeys(testData.BOB_TEST_USER_ID)).resolves.toBe(false);
});
});
describe("createRecoveryKeyFromPassphrase", () => {
let rustCrypto: RustCrypto;
beforeEach(async () => {
rustCrypto = await makeTestRustCrypto();
});
it("should create a recovery key without password", async () => {
const recoveryKey = await rustCrypto.createRecoveryKeyFromPassphrase();
// Expected the encoded private key to have 59 chars
expect(recoveryKey.encodedPrivateKey?.length).toBe(59);
// Expect the private key to be an Uint8Array with a length of 32
expect(recoveryKey.privateKey).toBeInstanceOf(Uint8Array);
expect(recoveryKey.privateKey.length).toBe(32);
// Expect keyInfo to be empty
expect(Object.keys(recoveryKey.keyInfo!).length).toBe(0);
});
it("should create a recovery key with password", async () => {
const recoveryKey = await rustCrypto.createRecoveryKeyFromPassphrase("my password");
// Expected the encoded private key to have 59 chars
expect(recoveryKey.encodedPrivateKey?.length).toBe(59);
// Expect the private key to be an Uint8Array with a length of 32
expect(recoveryKey.privateKey).toBeInstanceOf(Uint8Array);
expect(recoveryKey.privateKey.length).toBe(32);
// Expect keyInfo.passphrase to be filled
expect(recoveryKey.keyInfo?.passphrase?.algorithm).toBe("m.pbkdf2");
expect(recoveryKey.keyInfo?.passphrase?.iterations).toBe(500000);
});
});
it("should wait for a keys/query before returning devices", async () => {
jest.useFakeTimers();
const mockHttpApi = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
});
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: {
[testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA,
},
},
});
const rustCrypto = await makeTestRustCrypto(mockHttpApi, testData.TEST_USER_ID);
// an attempt to fetch the device list should block
const devicesPromise = rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
// ... until a /sync completes, and we trigger the outgoingRequests.
rustCrypto.onSyncCompleted({});
const deviceMap = (await devicesPromise).get(testData.TEST_USER_ID)!;
expect(deviceMap.has(TEST_DEVICE_ID)).toBe(true);
expect(deviceMap.has(testData.TEST_DEVICE_ID)).toBe(true);
rustCrypto.stop();
});
describe("requestDeviceVerification", () => {
it("throws an error if the device is unknown", async () => {
const rustCrypto = await makeTestRustCrypto();
await expect(() => rustCrypto.requestDeviceVerification(TEST_USER, "unknown")).rejects.toThrow(
"Not a known device",
);
});
});
describe("get|storeSessionBackupPrivateKey", () => {
it("can save and restore a key", async () => {
const key = "testtesttesttesttesttesttesttest";
const rustCrypto = await makeTestRustCrypto();
await rustCrypto.storeSessionBackupPrivateKey(new TextEncoder().encode(key));
const fetched = await rustCrypto.getSessionBackupPrivateKey();
expect(new TextDecoder().decode(fetched!)).toEqual(key);
});
});
describe("getActiveSessionBackupVersion", () => {
it("returns null", async () => {
const rustCrypto = await makeTestRustCrypto();
expect(await rustCrypto.getActiveSessionBackupVersion()).toBeNull();
});
});
describe("findVerificationRequestDMInProgress", () => {
it("throws an error if the userId is not provided", async () => {
const rustCrypto = await makeTestRustCrypto();
expect(() => rustCrypto.findVerificationRequestDMInProgress(testData.TEST_ROOM_ID)).toThrow(
"missing userId",
);
});
});
describe("requestVerificationDM", () => {
it("send verification request to an unknown user", async () => {
const rustCrypto = await makeTestRustCrypto();
await expect(() =>
rustCrypto.requestVerificationDM("@bob:example.com", testData.TEST_ROOM_ID),
).rejects.toThrow("unknown userId @bob:example.com");
});
});
describe("getUserVerificationStatus", () => {
let rustCrypto: RustCrypto;
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(() => {
olmMachine = {
getIdentity: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
olmMachine,
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
});
it("returns an unverified UserVerificationStatus when there is no UserIdentity", async () => {
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.isVerified()).toBeFalsy();
expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeFalsy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
});
it("returns a verified UserVerificationStatus when the UserIdentity is verified", async () => {
olmMachine.getIdentity.mockResolvedValue({ isVerified: jest.fn().mockReturnValue(true) });
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.isVerified()).toBeTruthy();
expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
});
});
});
/** build a basic RustCrypto instance for testing
@@ -322,6 +810,7 @@ async function makeTestRustCrypto(
userId: string = TEST_USER,
deviceId: string = TEST_DEVICE_ID,
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
): Promise<RustCrypto> {
return await initRustCrypto(http, userId, deviceId, secretStorage);
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks, null);
}
@@ -0,0 +1,69 @@
/*
Copyright 2023 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 { secretStorageContainsCrossSigningKeys } from "../../../src/rust-crypto/secret-storage";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
describe("secret-storage", () => {
describe("secretStorageContainsCrossSigningKeys", () => {
it("should return false when the master cross-signing key is not stored in secret storage", async () => {
const secretStorage = {
isStored: jest.fn().mockReturnValue(false),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});
it("should return false when there is no shared secret storage key between master, user signing and self signing keys", async () => {
const secretStorage = {
isStored: (type: string) => {
// Return different storage keys
if (type === "m.cross_signing.master") return { secretStorageKey: {} };
else return { secretStorageKey2: {} };
},
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});
it("should return false when the secret storage key for the master key is only shared by the user-signing key", async () => {
const secretStorage = {
isStored: (type: string) => {
// Return different storage keys
if (type === "m.cross_signing.master" || type === "m.cross_signing.user_signing") {
return { secretStorageKey: {} };
} else {
return { secretStorageKey2: {} };
}
},
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});
it("should return true when there is shared secret storage key between master, user signing and self signing keys", async () => {
const secretStorage = {
isStored: jest.fn().mockReturnValue({ secretStorageKey: {} }),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeTruthy();
});
});
});
+137
View File
@@ -0,0 +1,137 @@
/*
Copyright 2023 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 RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { Mocked } from "jest-mock";
import { isVerificationEvent, RustVerificationRequest } from "../../../src/rust-crypto/verification";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { EventType, MatrixEvent, MsgType } from "../../../src";
describe("VerificationRequest", () => {
describe("pending", () => {
let request: RustVerificationRequest;
let mockedInner: Mocked<RustSdkCryptoJs.VerificationRequest>;
beforeEach(() => {
mockedInner = makeMockedInner();
request = makeTestRequest(mockedInner);
});
it("returns true for a created request", () => {
expect(request.pending).toBe(true);
});
it("returns false for passive requests", () => {
mockedInner.isPassive.mockReturnValue(true);
expect(request.pending).toBe(false);
});
it("returns false for completed requests", () => {
mockedInner.phase.mockReturnValue(RustSdkCryptoJs.VerificationRequestPhase.Done);
expect(request.pending).toBe(false);
});
it("returns false for cancelled requests", () => {
mockedInner.phase.mockReturnValue(RustSdkCryptoJs.VerificationRequestPhase.Cancelled);
expect(request.pending).toBe(false);
});
});
describe("timeout", () => {
it("passes through the result", () => {
const mockedInner = makeMockedInner();
const request = makeTestRequest(mockedInner);
mockedInner.timeRemainingMillis.mockReturnValue(10_000);
expect(request.timeout).toEqual(10_000);
});
});
describe("startVerification", () => {
let request: RustVerificationRequest;
beforeEach(() => {
request = makeTestRequest();
});
it("does not permit methods other than SAS", async () => {
await expect(request.startVerification("m.reciprocate.v1")).rejects.toThrow(
"Unsupported verification method",
);
});
it("raises an error if starting verification does not produce a verifier", async () => {
await expect(request.startVerification("m.sas.v1")).rejects.toThrow(
"Still no verifier after startSas() call",
);
});
});
});
describe("isVerificationEvent", () => {
it.each([
[EventType.KeyVerificationCancel],
[EventType.KeyVerificationDone],
[EventType.KeyVerificationMac],
[EventType.KeyVerificationStart],
[EventType.KeyVerificationKey],
[EventType.KeyVerificationReady],
[EventType.KeyVerificationAccept],
])("should return true with %s event", (eventType) => {
const event = new MatrixEvent({
type: eventType,
});
expect(isVerificationEvent(event)).toBe(true);
});
it("should return true with EventType.RoomMessage and MsgType.KeyVerificationRequest", () => {
const event = new MatrixEvent({
type: EventType.RoomMessage,
content: {
msgtype: MsgType.KeyVerificationRequest,
},
});
expect(isVerificationEvent(event)).toBe(true);
});
it("should return false with a non verification event", () => {
const event = new MatrixEvent({
type: EventType.RoomName,
});
expect(isVerificationEvent(event)).toBe(false);
});
});
/** build a RustVerificationRequest with default parameters */
function makeTestRequest(
inner?: RustSdkCryptoJs.VerificationRequest,
outgoingRequestProcessor?: OutgoingRequestProcessor,
): RustVerificationRequest {
inner ??= makeMockedInner();
outgoingRequestProcessor ??= {} as OutgoingRequestProcessor;
return new RustVerificationRequest(inner, outgoingRequestProcessor, []);
}
/** Mock up a rust-side VerificationRequest */
function makeMockedInner(): Mocked<RustSdkCryptoJs.VerificationRequest> {
return {
registerChangesCallback: jest.fn(),
startSas: jest.fn(),
phase: jest.fn().mockReturnValue(RustSdkCryptoJs.VerificationRequestPhase.Created),
isPassive: jest.fn().mockReturnValue(false),
timeRemainingMillis: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.VerificationRequest>;
}
+27
View File
@@ -254,4 +254,31 @@ describe("IndexedDBStore", () => {
});
await expect(store.startup()).rejects.toThrow("Test");
});
it("remote worker should terminate upon destroy call", async () => {
const terminate = jest.fn();
const worker = new (class MockWorker {
private onmessage!: (data: any) => void;
postMessage(data: any) {
this.onmessage({
data: {
command: "cmd_success",
seq: data.seq,
result: [],
},
});
}
public terminate = terminate;
})() as unknown as Worker;
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
workerFactory: () => worker,
});
await store.startup();
await store.destroy();
expect(terminate).toHaveBeenCalled();
});
});
+327 -2
View File
@@ -16,8 +16,21 @@ limitations under the License.
*/
import { ReceiptType } from "../../src/@types/read_receipts";
import { IJoinedRoom, ISyncResponse, SyncAccumulator } from "../../src/sync-accumulator";
import {
IJoinedRoom,
IInvitedRoom,
IKnockedRoom,
IKnockState,
ILeftRoom,
IRoomEvent,
IStateEvent,
IStrippedState,
ISyncResponse,
SyncAccumulator,
IInviteState,
} from "../../src/sync-accumulator";
import { IRoomSummary } from "../../src";
import * as utils from "../test-utils/test-utils";
// The event body & unsigned object get frozen to assert that they don't get altered
// by the impl
@@ -95,6 +108,13 @@ describe("SyncAccumulator", function () {
},
},
},
knock: {
"!knock": {
knock_state: {
events: [member("alice", "knock")],
},
},
},
},
} as unknown as ISyncResponse;
sa.accumulate(res);
@@ -287,6 +307,268 @@ describe("SyncAccumulator", function () {
expect(sa.getJSON().accountData[0]).toEqual(acc2);
});
it("should delete invite room when invite request is rejected", () => {
const initInviteState: IInviteState = {
events: [
{
content: {
membership: "invite",
},
state_key: "bob",
sender: "alice",
type: "m.room.member",
},
],
};
sa.accumulate(
syncSkeleton(
{},
{
"!invite:bar": {
invite_state: initInviteState,
},
},
),
);
expect(sa.getJSON().roomsData.invite["!invite:bar"].invite_state).toBe(initInviteState);
const rejectMemberEvent: IStateEvent = {
event_id: "$" + Math.random(),
content: {
membership: "leave",
},
origin_server_ts: 123456789,
state_key: "bob",
sender: "bob",
type: "m.room.member",
unsigned: {
prev_content: {
membership: "invite",
},
},
};
const leftRoomState = leftRoomSkeleton([rejectMemberEvent]);
// bob rejects invite
sa.accumulate(
syncSkeleton(
{},
{},
{
"!invite:bar": leftRoomState,
},
),
);
expect(sa.getJSON().roomsData.invite["!invite:bar"]).toBeUndefined();
});
it("should accumulate knock state", () => {
const initKnockState = {
events: [member("alice", "knock")],
};
sa.accumulate(
syncSkeleton(
{},
{},
{},
{
knock_state: initKnockState,
},
),
);
expect(sa.getJSON().roomsData.knock["!knock:bar"].knock_state).toBe(initKnockState);
sa.accumulate(
syncSkeleton(
{},
{},
{},
{
knock_state: {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 1",
},
}) as IStrippedState,
],
},
},
),
);
expect(
sa.getJSON().roomsData.knock["!knock:bar"].knock_state.events.find((e) => e.type === "m.room.name")?.content
.name,
).toEqual("Room 1");
sa.accumulate(
syncSkeleton(
{},
{},
{},
{
knock_state: {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 2",
},
}) as IStrippedState,
],
},
},
),
);
expect(
sa.getJSON().roomsData.knock["!knock:bar"].knock_state.events.find((e) => e.type === "m.room.name")?.content
.name,
).toEqual("Room 2");
});
it("should delete knocked room when knock request is approved", () => {
const initKnockState = makeKnockState();
sa.accumulate(
syncSkeleton(
{},
{},
{},
{
knock_state: initKnockState,
},
),
);
expect(sa.getJSON().roomsData.knock["!knock:bar"].knock_state).toBe(initKnockState);
// alice approves bob's knock request
const inviteStateEvents = [
{
content: {
membership: "invite",
},
state_key: "bob",
sender: "alice",
type: "m.room.member",
},
];
sa.accumulate(
syncSkeleton(
{},
{
"!knock:bar": {
invite_state: {
events: inviteStateEvents,
},
},
},
),
);
expect(sa.getJSON().roomsData.knock["!knock:bar"]).toBeUndefined();
expect(sa.getJSON().roomsData.invite["!knock:bar"].invite_state.events).toEqual(inviteStateEvents);
});
it("should delete knocked room when knock request is cancelled by user himself", () => {
// bob cancels his knock state
const memberEvent: IStateEvent = {
event_id: "$" + Math.random(),
content: {
membership: "leave",
},
origin_server_ts: 123456789,
state_key: "bob",
sender: "bob",
type: "m.room.member",
unsigned: {
prev_content: {
membership: "knock",
},
},
};
const leftRoomState = leftRoomSkeleton([memberEvent]);
const initKnockState = makeKnockState();
sa.accumulate(
syncSkeleton(
{},
{},
{},
{
knock_state: initKnockState,
},
),
);
expect(sa.getJSON().roomsData.knock["!knock:bar"].knock_state).toBe(initKnockState);
// bob cancels his knock request
sa.accumulate(
syncSkeleton(
{},
{},
{
"!knock:bar": leftRoomState,
},
),
);
expect(sa.getJSON().roomsData.knock["!knock:bar"]).toBeUndefined();
});
it("should delete knocked room when knock request is denied by another user", () => {
// alice denies bob knock state
const memberEvent: IStateEvent = {
event_id: "$" + Math.random(),
content: {
membership: "leave",
},
origin_server_ts: 123456789,
state_key: "bob",
sender: "alice",
type: "m.room.member",
unsigned: {
prev_content: {
membership: "knock",
},
},
};
const leftRoomState = leftRoomSkeleton([memberEvent]);
const initKnockState = makeKnockState();
sa.accumulate(
syncSkeleton(
{},
{},
{},
{
knock_state: initKnockState,
},
),
);
expect(sa.getJSON().roomsData.knock["!knock:bar"].knock_state).toBe(initKnockState);
// alice denies bob's knock request
sa.accumulate(
syncSkeleton(
{},
{},
{
"!knock:bar": leftRoomState,
},
),
);
expect(sa.getJSON().roomsData.knock["!knock:bar"]).toBeUndefined();
});
it("should accumulate read receipts", () => {
const receipt1 = {
type: "m.receipt",
@@ -601,7 +883,12 @@ describe("SyncAccumulator", function () {
});
});
function syncSkeleton(joinObj: Partial<IJoinedRoom>): ISyncResponse {
function syncSkeleton(
joinObj: Partial<IJoinedRoom>,
invite?: Record<string, IInvitedRoom>,
leave?: Record<string, ILeftRoom>,
knockObj?: Partial<IKnockedRoom>,
): ISyncResponse {
joinObj = joinObj || {};
return {
next_batch: "abc",
@@ -609,10 +896,48 @@ function syncSkeleton(joinObj: Partial<IJoinedRoom>): ISyncResponse {
join: {
"!foo:bar": joinObj,
},
invite,
leave,
knock: knockObj
? {
"!knock:bar": knockObj,
}
: undefined,
},
} as unknown as ISyncResponse;
}
function leftRoomSkeleton(timelineEvents: Array<IRoomEvent | IStateEvent> = []): ILeftRoom {
return {
state: {
events: [],
},
timeline: {
events: timelineEvents,
prev_batch: "something",
},
account_data: {
events: [],
},
};
}
function makeKnockState(): IKnockState {
return {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room",
},
}) as IStrippedState,
member("bob", "knock"),
],
};
}
function msg(localpart: string, text: string) {
return {
event_id: "$" + Math.random(),
+17 -2
View File
@@ -30,14 +30,14 @@ import {
sortEventsByLatestContentTimestamp,
safeSet,
MapWithDefault,
globToRegexp,
escapeRegExp,
} from "../../src/utils";
import { logger } from "../../src/logger";
import { mkMessage } from "../test-utils/test-utils";
import { makeBeaconEvent } from "../test-utils/beacon";
import { ReceiptType } from "../../src/@types/read_receipts";
// TODO: Fix types throughout
describe("utils", function () {
describe("encodeParams", function () {
it("should url encode and concat with &s", function () {
@@ -725,4 +725,19 @@ describe("utils", function () {
await utils.immediate();
});
});
describe("escapeRegExp", () => {
it("should escape XYZ", () => {
expect(escapeRegExp("[FIT-Connect Zustelldienst \\(Testumgebung\\)]")).toMatchInlineSnapshot(
`"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`,
);
});
});
describe("globToRegexp", () => {
it("should not explode when given regexes as globs", () => {
const result = globToRegexp("[FIT-Connect Zustelldienst \\(Testumgebung\\)]");
expect(result).toMatchInlineSnapshot(`"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`);
});
});
});
+29 -9
View File
@@ -47,6 +47,7 @@ import {
} from "../../test-utils/webrtc";
import { CallFeed } from "../../../src/webrtc/callFeed";
import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
const FAKE_ROOM_ID = "!foo:bar";
const CALL_LIFETIME = 60000;
@@ -1054,14 +1055,7 @@ describe("Call", function () {
mockSendEvent.mockReset();
let caught = false;
try {
call.reject();
} catch (e) {
caught = true;
}
expect(caught).toEqual(true);
expect(() => call.reject()).toThrow();
expect(client.client.sendEvent).not.toHaveBeenCalled();
call.hangup(CallErrorCode.UserHangup, true);
@@ -1614,7 +1608,7 @@ describe("Call", function () {
it("throws when there is no error listener", async () => {
call.off(CallEvent.Error, errorListener);
expect(call.placeVoiceCall()).rejects.toThrow();
await expect(call.placeVoiceCall()).rejects.toThrow();
});
describe("hasPeerConnection()", () => {
@@ -1819,4 +1813,30 @@ describe("Call", function () {
expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled();
});
});
it("should emit IceFailed error on the successor call if RTCPeerConnection throws", async () => {
// @ts-ignore - writing to window as we are simulating browser edge-cases
global.window = {};
Object.defineProperty(global.window, "RTCPeerConnection", {
get: () => {
throw Error("Secure mode, naaah!");
},
});
const call = new MatrixCall({
client: client.client,
roomId: "!room_id",
});
const successor = new MatrixCall({
client: client.client,
roomId: "!room_id",
});
call.replacedBy(successor);
const prom = emitPromise(successor, CallEvent.Error);
call.placeCall(true, true);
const err = await prom;
expect(err.code).toBe(CallErrorCode.IceFailed);
});
});
+100 -15
View File
@@ -18,25 +18,25 @@ import { mocked } from "jest-mock";
import { EventType, GroupCallIntent, GroupCallType, MatrixCall, MatrixEvent, Room, RoomMember } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall";
import { GroupCall, GroupCallEvent, GroupCallState, GroupCallStatsReportEvent } from "../../../src/webrtc/groupCall";
import { IMyDevice, MatrixClient } from "../../../src/client";
import {
FAKE_CONF_ID,
FAKE_DEVICE_ID_1,
FAKE_DEVICE_ID_2,
FAKE_ROOM_ID,
FAKE_SESSION_ID_1,
FAKE_SESSION_ID_2,
FAKE_USER_ID_1,
FAKE_USER_ID_2,
FAKE_USER_ID_3,
installWebRTCMocks,
MockCallFeed,
MockCallMatrixClient,
MockMatrixCall,
MockMediaStream,
MockMediaStreamTrack,
MockRTCPeerConnection,
MockMatrixCall,
FAKE_ROOM_ID,
FAKE_USER_ID_1,
FAKE_CONF_ID,
FAKE_DEVICE_ID_2,
FAKE_SESSION_ID_2,
FAKE_USER_ID_2,
FAKE_DEVICE_ID_1,
FAKE_SESSION_ID_1,
FAKE_USER_ID_3,
} from "../../test-utils/webrtc";
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
import { sleep } from "../../../src/utils";
@@ -44,6 +44,9 @@ import { CallEventHandlerEvent } from "../../../src/webrtc/callEventHandler";
import { CallFeed } from "../../../src/webrtc/callFeed";
import { CallEvent, CallState } from "../../../src/webrtc/call";
import { flushPromises } from "../../test-utils/flushPromises";
import { CallFeedReport } from "../../../src/webrtc/stats/statsReport";
import { CallFeedStatsReporter } from "../../../src/webrtc/stats/callFeedStatsReporter";
import { StatsReportEmitter } from "../../../src/webrtc/stats/statsReportEmitter";
const FAKE_STATE_EVENTS = [
{
@@ -186,10 +189,7 @@ describe("Group Call", function () {
it("sets state to local call feed uninitialized when getUserMedia() fails", async () => {
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValue("Error");
try {
await groupCall.initLocalCallFeed();
} catch (e) {}
await expect(groupCall.initLocalCallFeed()).rejects.toBeTruthy();
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
});
@@ -1729,4 +1729,89 @@ describe("Group Call", function () {
expect(start).toHaveBeenCalled();
});
});
describe("as stats event listener and a CallFeedReport was triggered", () => {
let groupCall: GroupCall;
let reportEmitter: StatsReportEmitter;
const report: CallFeedReport = {} as CallFeedReport;
beforeEach(async () => {
CallFeedStatsReporter.expandCallFeedReport = jest.fn().mockReturnValue(report);
const typedMockClient = new MockCallMatrixClient(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, FAKE_SESSION_ID_1);
const mockClient = typedMockClient.typed();
const room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
} as unknown as RoomMember;
room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
} as unknown as RoomMember;
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
groupCall = await createAndEnterGroupCall(mockClient, room);
reportEmitter = groupCall.getGroupCallStats().reports;
});
it("should not extends with feed stats if no call exists", async () => {
const testPromise = new Promise<void>((done) => {
groupCall.on(GroupCallStatsReportEvent.CallFeedStats, () => {
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith({}, [], "from-call-feed");
done();
});
});
const report: CallFeedReport = {} as CallFeedReport;
reportEmitter.emitCallFeedReport(report);
await testPromise;
});
it("and a CallFeedReport was triggered then it should extends with local feed", async () => {
const localCallFeed = {} as CallFeed;
groupCall.localCallFeed = localCallFeed;
const testPromise = new Promise<void>((done) => {
groupCall.on(GroupCallStatsReportEvent.CallFeedStats, () => {
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
report,
[localCallFeed],
"from-local-feed",
);
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
report,
[],
"from-call-feed",
);
done();
});
});
const report: CallFeedReport = {} as CallFeedReport;
reportEmitter.emitCallFeedReport(report);
await testPromise;
});
it("and a CallFeedReport was triggered then it should extends with remote feed", async () => {
const localCallFeed = {} as CallFeed;
groupCall.localCallFeed = localCallFeed;
// @ts-ignore Suppress error because access to private property
const call = groupCall.calls.get(FAKE_USER_ID_2)!.get(FAKE_DEVICE_ID_2)!;
report.callId = call.callId;
const feeds = call.getFeeds();
const testPromise = new Promise<void>((done) => {
groupCall.on(GroupCallStatsReportEvent.CallFeedStats, () => {
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
report,
[localCallFeed],
"from-local-feed",
);
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
report,
feeds,
"from-call-feed",
);
done();
});
});
reportEmitter.emitCallFeedReport(report);
await testPromise;
});
});
});
@@ -71,7 +71,8 @@ describe("Group Call Event Handler", function () {
getMember: (userId: string) => (userId === FAKE_USER_ID ? mockMember : null),
} as unknown as Room;
(mockClient as any).getRoom = jest.fn().mockReturnValue(mockRoom);
mockClient.getRoom = jest.fn().mockReturnValue(mockRoom);
mockClient.getFoci.mockReturnValue([{}]);
});
describe("reacts to state changes", () => {
@@ -0,0 +1,110 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CallFeedStatsReporter should builds CallFeedReport 1`] = `
{
"callFeeds": [],
"callId": "CALL_ID",
"opponentMemberId": "USER_ID",
"transceiver": [
{
"currentDirection": "sendonly",
"direction": "sendrecv",
"mid": "0",
"receiver": {
"constrainDeviceId": "constrainDeviceId-receiver_audio_0",
"enabled": true,
"id": "receiver_audio_0",
"kind": "audio",
"label": "receiver",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-receiver_audio_0",
},
"sender": {
"constrainDeviceId": "constrainDeviceId-sender_audio_0",
"enabled": true,
"id": "sender_audio_0",
"kind": "audio",
"label": "sender",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-sender_audio_0",
},
},
{
"currentDirection": "sendrecv",
"direction": "recvonly",
"mid": "1",
"receiver": {
"constrainDeviceId": "constrainDeviceId-receiver_video_1",
"enabled": true,
"id": "receiver_video_1",
"kind": "video",
"label": "receiver",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-receiver_video_1",
},
"sender": {
"constrainDeviceId": "constrainDeviceId-sender_video_1",
"enabled": true,
"id": "sender_video_1",
"kind": "video",
"label": "sender",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-sender_video_1",
},
},
{
"currentDirection": "recvonly",
"direction": "recvonly",
"mid": "2",
"receiver": {
"constrainDeviceId": "constrainDeviceId-receiver_video_2",
"enabled": true,
"id": "receiver_video_2",
"kind": "video",
"label": "receiver",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-receiver_video_2",
},
"sender": null,
},
],
}
`;
exports[`CallFeedStatsReporter should extends CallFeedReport with call feeds 1`] = `
[
{
"audio": {
"constrainDeviceId": "constrainDeviceId-video-1",
"enabled": true,
"id": "video-1",
"kind": "video",
"label": "--",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-video-1",
},
"isAudioMuted": true,
"isVideoMuted": false,
"prefix": "unknown",
"purpose": undefined,
"stream": "stream-1",
"type": "local",
"video": {
"constrainDeviceId": "constrainDeviceId-audio-1",
"enabled": true,
"id": "audio-1",
"kind": "audio",
"label": "--",
"muted": false,
"readyState": "live",
"settingDeviceId": "settingDeviceId-audio-1",
},
},
]
`;
@@ -0,0 +1,117 @@
/*
Copyright 2023 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 { CallFeedStatsReporter } from "../../../../src/webrtc/stats/callFeedStatsReporter";
import { CallFeedReport } from "../../../../src/webrtc/stats/statsReport";
import { CallFeed } from "../../../../src/webrtc/callFeed";
const CALL_ID = "CALL_ID";
const USER_ID = "USER_ID";
describe("CallFeedStatsReporter", () => {
let rtcSpy: RTCPeerConnection;
beforeEach(() => {
rtcSpy = {} as RTCPeerConnection;
rtcSpy.getTransceivers = jest.fn().mockReturnValue(buildTransceiverMocks());
});
describe("should", () => {
it("builds CallFeedReport", async () => {
expect(CallFeedStatsReporter.buildCallFeedReport(CALL_ID, USER_ID, rtcSpy)).toMatchSnapshot();
});
it("extends CallFeedReport with call feeds", async () => {
const feed = buildCallFeedMock("1");
const callFeedList: CallFeed[] = [feed];
const report = {
callId: "callId",
opponentMemberId: "opponentMemberId",
transceiver: [],
callFeeds: [],
} as CallFeedReport;
expect(CallFeedStatsReporter.expandCallFeedReport(report, callFeedList).callFeeds).toMatchSnapshot();
});
});
const buildTransceiverMocks = (): RTCRtpTransceiver[] => {
const trans1 = {
mid: "0",
direction: "sendrecv",
currentDirection: "sendonly",
sender: buildSenderMock("sender_audio_0", "audio"),
receiver: buildReceiverMock("receiver_audio_0", "audio"),
} as RTCRtpTransceiver;
const trans2 = {
mid: "1",
direction: "recvonly",
currentDirection: "sendrecv",
sender: buildSenderMock("sender_video_1", "video"),
receiver: buildReceiverMock("receiver_video_1", "video"),
} as RTCRtpTransceiver;
const trans3 = {
mid: "2",
direction: "recvonly",
currentDirection: "recvonly",
sender: { track: null } as RTCRtpSender,
receiver: buildReceiverMock("receiver_video_2", "video"),
} as RTCRtpTransceiver;
return [trans1, trans2, trans3];
};
const buildSenderMock = (id: string, kind: "audio" | "video"): RTCRtpSender => {
const track = buildTrackMock(id, kind);
return {
track,
} as RTCRtpSender;
};
const buildReceiverMock = (id: string, kind: "audio" | "video"): RTCRtpReceiver => {
const track = buildTrackMock(id, kind);
return {
track,
} as RTCRtpReceiver;
};
const buildTrackMock = (id: string, kind: "audio" | "video"): MediaStreamTrack => {
return {
id,
kind,
enabled: true,
label: "--",
muted: false,
readyState: "live",
getSettings: () => ({ deviceId: `settingDeviceId-${id}` }),
getConstraints: () => ({ deviceId: `constrainDeviceId-${id}` }),
} as MediaStreamTrack;
};
const buildCallFeedMock = (id: string, isLocal = true): CallFeed => {
const stream = {
id: `stream-${id}`,
getAudioTracks(): MediaStreamTrack[] {
return [buildTrackMock(`video-${id}`, "video")];
},
getVideoTracks(): MediaStreamTrack[] {
return [buildTrackMock(`audio-${id}`, "audio")];
},
} as MediaStream;
return {
stream,
isLocal: () => isLocal,
isVideoMuted: () => false,
isAudioMuted: () => true,
} as CallFeed;
};
});
@@ -0,0 +1,256 @@
/*
Copyright 2023 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 { CallStatsReportGatherer } from "../../../../src/webrtc/stats/callStatsReportGatherer";
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
import { MediaSsrcHandler } from "../../../../src/webrtc/stats/media/mediaSsrcHandler";
import { currentChromeReport, prevChromeReport } from "../../../test-utils/webrtcReports";
import { MockRTCPeerConnection } from "../../../test-utils/webrtc";
const CALL_ID = "CALL_ID";
const USER_ID = "USER_ID";
describe("CallStatsReportGatherer", () => {
let collector: CallStatsReportGatherer;
let rtcSpy: RTCPeerConnection;
let emitter: StatsReportEmitter;
beforeEach(() => {
rtcSpy = { getStats: () => new Promise<RTCStatsReport>(() => null) } as RTCPeerConnection;
rtcSpy.addEventListener = jest.fn();
rtcSpy.getTransceivers = jest.fn().mockReturnValue([]);
emitter = new StatsReportEmitter();
collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
});
describe("on process stats", () => {
it("if active calculate stats reports", async () => {
const getStats = jest.spyOn(rtcSpy, "getStats");
const report = {} as RTCStatsReport;
report.forEach = jest.fn().mockReturnValue([]);
getStats.mockResolvedValue(report);
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(getStats).toHaveBeenCalled();
expect(actual).toEqual({
isFirstCollection: true,
receivedMedia: 0,
receivedAudioMedia: 0,
receivedVideoMedia: 0,
audioTrackSummary: {
count: 0,
muted: 0,
maxJitter: 0,
maxPacketLoss: 0,
concealedAudio: 0,
totalAudio: 0,
},
videoTrackSummary: {
count: 0,
muted: 0,
maxJitter: 0,
maxPacketLoss: 0,
concealedAudio: 0,
totalAudio: 0,
},
});
expect(collector.getActive()).toBeTruthy();
});
it("if not active do not calculate stats reports", async () => {
collector.setActive(false);
const getStats = jest.spyOn(rtcSpy, "getStats");
await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(getStats).not.toHaveBeenCalled();
});
it("if get reports fails, the collector becomes inactive", async () => {
expect(collector.getActive()).toBeTruthy();
const getStats = jest.spyOn(rtcSpy, "getStats");
getStats.mockRejectedValue(new Error("unknown"));
await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(getStats).toHaveBeenCalled();
expect(collector.getActive()).toBeFalsy();
});
it("if active and getStats returns not an RTCStatsReport inside a promise the collector fails and becomes inactive", async () => {
const getStats = jest.spyOn(rtcSpy, "getStats");
// @ts-ignore
getStats.mockReturnValue({});
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(actual).toEqual({
isFirstCollection: true,
receivedMedia: 0,
receivedAudioMedia: 0,
receivedVideoMedia: 0,
audioTrackSummary: {
count: 0,
muted: 0,
maxJitter: 0,
maxPacketLoss: 0,
concealedAudio: 0,
totalAudio: 0,
},
videoTrackSummary: {
count: 0,
muted: 0,
maxJitter: 0,
maxPacketLoss: 0,
concealedAudio: 0,
totalAudio: 0,
},
});
expect(getStats).toHaveBeenCalled();
expect(collector.getActive()).toBeFalsy();
});
it("if active and the collector runs not the first time the Summery Stats is marked as not fits collection", async () => {
const getStats = jest.spyOn(rtcSpy, "getStats");
// @ts-ignore
collector.previousStatsReport = {} as RTCStatsReport;
const report = {} as RTCStatsReport;
report.forEach = jest.fn().mockReturnValue([]);
getStats.mockResolvedValue(report);
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(getStats).toHaveBeenCalled();
expect(actual).toEqual({
isFirstCollection: false,
receivedMedia: 0,
receivedAudioMedia: 0,
receivedVideoMedia: 0,
audioTrackSummary: {
count: 0,
muted: 0,
maxJitter: 0,
maxPacketLoss: 0,
concealedAudio: 0,
totalAudio: 0,
},
videoTrackSummary: {
count: 0,
muted: 0,
maxJitter: 0,
maxPacketLoss: 0,
concealedAudio: 0,
totalAudio: 0,
},
});
expect(collector.getActive()).toBeTruthy();
});
describe("should not only produce a call summary stat but also", () => {
const wantedSummaryReport = {
isFirstCollection: false,
audioTrackSummary: {
concealedAudio: 0,
count: 0,
maxJitter: 0,
maxPacketLoss: 0,
muted: 0,
totalAudio: 0,
},
receivedAudioMedia: 0,
receivedMedia: 0,
receivedVideoMedia: 0,
videoTrackSummary: {
concealedAudio: 0,
count: 0,
maxJitter: 0,
maxPacketLoss: 0,
muted: 0,
totalAudio: 0,
},
};
beforeEach(() => {
rtcSpy = new MockRTCPeerConnection() as unknown as RTCPeerConnection;
collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
const getStats = jest.spyOn(rtcSpy, "getStats");
const previous = prevChromeReport as unknown as RTCStatsReport;
previous.get = (id: string) => {
return prevChromeReport.find((data) => data.id === id);
};
// @ts-ignore
collector.previousStatsReport = previous;
const current = currentChromeReport as unknown as RTCStatsReport;
current.get = (id: string) => {
return currentChromeReport.find((data) => data.id === id);
};
// @ts-ignore
getStats.mockResolvedValue(current);
});
it("emit byteSentStatsReport", async () => {
const emitByteSendReport = jest.spyOn(emitter, "emitByteSendReport");
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(actual).toEqual(wantedSummaryReport);
expect(emitByteSendReport).toHaveBeenCalled();
});
it("emit emitConnectionStatsReport", async () => {
const emitConnectionStatsReport = jest.spyOn(emitter, "emitConnectionStatsReport");
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(actual).toEqual(wantedSummaryReport);
expect(emitConnectionStatsReport).toHaveBeenCalled();
});
it("emit callFeedStatsReport", async () => {
const emitCallFeedReport = jest.spyOn(emitter, "emitCallFeedReport");
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(actual).toEqual(wantedSummaryReport);
expect(emitCallFeedReport).toHaveBeenCalled();
});
});
});
describe("on signal state change event", () => {
let events: { [key: string]: any };
beforeEach(() => {
events = [];
// Define the addEventListener method with a Jest mock function
rtcSpy.addEventListener = jest.fn((event: any, callback: any) => {
events[event] = callback;
});
collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
});
it("in case of stable, parse remote and local description", () => {
// @ts-ignore
const mediaSsrcHandler = {
parse: jest.fn(),
ssrcToMid: jest.fn(),
findMidBySsrc: jest.fn(),
getSsrcToMidMap: jest.fn(),
} as MediaSsrcHandler;
const remoteSDP = "sdp";
const localSDP = "sdp";
// @ts-ignore
rtcSpy.signalingState = "stable";
// @ts-ignore
rtcSpy.currentRemoteDescription = <RTCSessionDescription>{ sdp: remoteSDP };
// @ts-ignore
rtcSpy.currentLocalDescription = <RTCSessionDescription>{ sdp: localSDP };
// @ts-ignore
collector.trackStats.mediaSsrcHandler = mediaSsrcHandler;
events["signalingstatechange"]();
expect(mediaSsrcHandler.parse).toHaveBeenCalledWith(remoteSDP, "remote");
expect(mediaSsrcHandler.parse).toHaveBeenCalledWith(localSDP, "local");
});
});
});
@@ -16,7 +16,7 @@ limitations under the License.
import { TrackID } from "../../../../src/webrtc/stats/statsReport";
import { MediaTrackStats } from "../../../../src/webrtc/stats/media/mediaTrackStats";
import { StatsReportBuilder } from "../../../../src/webrtc/stats/statsReportBuilder";
import { ConnectionStatsReportBuilder } from "../../../../src/webrtc/stats/connectionStatsReportBuilder";
describe("StatsReportBuilder", () => {
const LOCAL_VIDEO_TRACK_ID = "LOCAL_VIDEO_TRACK_ID";
@@ -39,7 +39,7 @@ describe("StatsReportBuilder", () => {
describe("should build stats", () => {
it("by media track stats.", async () => {
expect(StatsReportBuilder.build(stats)).toEqual({
expect(ConnectionStatsReportBuilder.build(stats)).toEqual({
bitrate: {
audio: {
download: 4000,

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