Compare commits

...

221 Commits

Author SHA1 Message Date
Michael Telatynski 14b2ee2da4 Remove dependency on uuid (#5297)
Notify Downstream Projects / notify-downstream (element-web-notify, element-hq/element-web) (push) Has been skipped
Static Analysis / Typescript Syntax Check (push) Failing after 1m36s
Static Analysis / ESLint (push) Failing after 31s
Static Analysis / Node.js example (push) Failing after 42s
Static Analysis / Workflow Lint (push) Failing after 9m59s
Static Analysis / JSDoc Checker (push) Failing after 50s
Static Analysis / Analyse Dead Code (push) Failing after 36s
Static Analysis / Downstream tsc element-web (push) Has been skipped
Tests / Vitest [integ] (Node 22) (push) Failing after 4s
Tests / Vitest [unit] (Node 22) (push) Failing after 39s
Tests / Vitest [integ] (Node lts/*) (push) Failing after 39s
Tests / Vitest [unit] (Node lts/*) (push) Failing after 35s
Tests / Downstream test element-web (push) Has been skipped
Tests / Run Complement Crypto tests (push) Has been skipped
Static Analysis / Static Analysis (push) Successful in 1s
Tests / Tests (push) Failing after 1s
Tests / Downstream Complement Crypto tests (push) Successful in 1s
Tests / Downstream tests (push) Successful in 4s
2026-04-23 09:53:18 +00:00
renovate[bot] bb083222d9 Update dependency vite to v8 (#5295)
* Update dependency vite to v8

* Update lockfile

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-04-22 16:16:29 +00:00
Andy Balaam fa424c44b4 Support stable identifiers for MSC4268 (#5290)
* Support stable identifier m.room_key_bundle

* Support stable identifier m.shared_history

* Test that checks isRoomKeyBundleMessage works for stable and unstable identifiers

* Replace similar tests with use of it.each
2026-04-22 09:10:39 +00:00
renovate[bot] fef093747e Update dependency @typescript-eslint/eslint-plugin to v8.59.0 (#5292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 14:38:05 +00:00
renovate[bot] 4b33892d48 Update npm non-major dependencies (#5293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 13:58:22 +00:00
renovate[bot] d7d771fadb Update vite to v4.1.5 (#5291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 12:37:38 +00:00
Hubert Chathi 4ee3e591bf Handle secret pushing for key backups (#5189)
* push backup key to other verified devices when we reset backup

* handle receiving pushed backup keys

- make sure that backup gets enabled after we receive a pushed key that
  matches the current, valid backup

* apply requested changes from review
2026-04-20 21:48:51 +00:00
renovate[bot] 668183d722 Update vite to v4.1.4 (#5289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 11:25:37 +00:00
renovate[bot] 854dae0dc0 Update typescript (#5288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 10:01:43 +00:00
renovate[bot] 9d1aca2232 Update eslint-plugins (#5286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 09:11:28 +00:00
renovate[bot] 81569f3461 Update actions/setup-node digest to 48b55a0 (#5285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 08:50:10 +00:00
renovate[bot] 50783aba76 Update npm non-major dependencies (#5287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 08:17:20 +00:00
Michael Telatynski fd01b17236 Tidy knip config (#5284) 2026-04-17 13:00:21 +00:00
Will Hunt 25e92009b7 Throw more helpful errors from indexedb-local-backend (#5282)
* Throw more helpful errors from indexedb-local-backend

* Add a test

* Do not tempt fate
2026-04-17 08:33:56 +00:00
Michael Telatynski eb7acfb810 Add support for m.recent_emoji account data event (#5280) 2026-04-16 21:59:30 +00:00
Andy Balaam ca5655bced Rotate the room key when anyone leaves a room for any reason (#5279) 2026-04-16 12:09:42 +00:00
Michael Telatynski ef9b13e2a6 Fix coverage reports clobbering each other (#5267)
merge-multiple would silently drop files with clashing names - it ultimately isn't necessary given the `find` command will happily find them in nested subdirs
2026-04-16 11:48:51 +00:00
fkwp 159cca0363 Adapt LiveKit Identity hash calculation to latest MSC4195 update (#5268)
* adapt hash calculation to latest MSC4195 update.

Stop using '|' delimiters in hashes; use JSON arrays + canonical JSON instead

Signed-off-by: fkwp <github-fkwp@w4ve.de>

* Update the test for RTC backend identities and add tests for calculating the identity hash.

Signed-off-by: fkwp <github-fkwp@w4ve.de>

* linting

Signed-off-by: fkwp <github-fkwp@w4ve.de>

* add copyright header

---------

Signed-off-by: fkwp <github-fkwp@w4ve.de>
2026-04-16 09:48:11 +00:00
renovate[bot] 3879111850 Update typescript (#5274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 11:37:50 +00:00
Andy Balaam f9a5aa87e3 Support the stable prefix for MSC4287 (key backup preference) (#5258)
* Support the stable prefix for MSC4287 (key backup preference)

* Remove incorrect doc coment on disableKeyStorage
2026-04-15 11:07:48 +00:00
renovate[bot] ed58df040c Update eslint-plugins (#5276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 09:51:25 +00:00
Michael Telatynski 0a3448d4c9 Fix references to matrix-react-sdk in CI (#5269) 2026-04-15 09:25:54 +00:00
renovate[bot] 25c1c1ea26 Update actions/github-script action to v9 (#5277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 09:11:09 +00:00
renovate[bot] a5e67af31f Update actions/upload-pages-artifact action to v5 (#5278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:37:38 +00:00
renovate[bot] b91e80814a Update zizmorcore/zizmor-action action to v0.5.3 (#5275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:37:07 +00:00
renovate[bot] d096a72605 Update npm non-major dependencies (#5273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:36:56 +00:00
renovate[bot] fb547e7b4b Update actions/upload-artifact digest to 043fb46 (#5272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:36:46 +00:00
Michael Telatynski 815294ca5a Allow oidc jwks_uri to be omitted (#5271)
* Allow oidc jwks_uri to be null

As signing keys are seemingly not (yet?) in the spec

* Add test coverage
2026-04-15 08:15:29 +00:00
Michael Telatynski 6d270b4685 Handle response_mode=fragment in completeAuthorizationCodeGrant (#5266)
* Handle response_mode=fragment in completeAuthorizationCodeGrant

* Add test

* Fix docs
2026-04-14 14:59:10 +00:00
Michael Telatynski 8bc3d96f6b Allow generating OIDC URIs with response_mode=fragment (#5265) 2026-04-13 22:53:05 +00:00
Richard van der Hoff b6ea6e105e Log clarifications for handleBackupSecretReceived (#5233)
Some changes to make this a bit easier to understand.
2026-04-10 12:33:30 +00:00
Valere Fedronic cd4e053fa5 Suppress and Reduce noisy logs for rtc (#5260) 2026-04-09 14:24:36 +00:00
Andy Balaam 727473af62 Expand the comment on CryptoApi.getUserDeviceInfo saying we request info from the server (#5256)
* Expand the comment on CryptoApi.getUserDeviceInfo saying we request info from the server

* Update comment to reflect waiting for in-progress requests, not making new ones

* Update the comment for userHasCrossSigningKeys too
2026-04-09 14:19:22 +00:00
Michael Telatynski f17f013f1e Satisfy pnpm audit (#5262) 2026-04-09 11:52:19 +00:00
renovate[bot] 9f4ab0b840 Update matrix-org/sonarcloud-workflow-action digest to 13968a2 (#5263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 13:28:46 +00:00
Michael Telatynski 6371e4b252 Update pnpm command for version bumping 2026-04-08 08:23:52 +01:00
Michael Telatynski b69929e01a Allow release-make to bump multiple package.json versions (#5261) 2026-04-07 14:44:15 +00:00
RiotRobot 9dc12baaa9 Merge branch 'master' into develop 2026-04-07 13:17:46 +00:00
RiotRobot 159738597d v41.3.0 2026-04-07 13:17:11 +00:00
Andy Balaam d02205652f Re-enable Complement Crypto tests (#5257)
Since https://github.com/matrix-org/complement-crypto/pull/235 these
should be more reliable.

This reverts commit 4d59291538.
2026-04-01 12:38:11 +00:00
Richard van der Hoff 5e03add29a Expose UserVerificationStatus.known flag (#5255)
Indicate whether we have a record of this user's identity.
2026-04-01 09:58:50 +00:00
renovate[bot] eeafd7fcaa Update npm non-major dependencies (#5251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 16:36:53 +00:00
renovate[bot] 78a3c5372d Update vite (#5253)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:58:28 +00:00
renovate[bot] c0c3bc2a8c Update dependency p-retry to v8 (#5254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:28:47 +00:00
renovate[bot] c3ce49cabf Update pnpm to v10.33.0 (#5252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:19:11 +00:00
renovate[bot] 5408168dfd Update dependency eslint-plugin-jsdoc to v62.8.1 (#5250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:17:51 +00:00
renovate[bot] 61452ddc11 Update pnpm/action-setup action to v5 (#5239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:11:15 +00:00
Michael Telatynski d5160a5380 Add permissions for contents in workflow 2026-03-31 14:36:05 +01:00
Michael Telatynski 7ff4960a27 Update workflow to use build-and-test.yaml from EW
Moved in https://github.com/element-hq/element-web/pull/32929
2026-03-31 14:34:06 +01:00
RiotRobot 00f63db80f v41.3.0-rc.0 2026-03-31 12:33:30 +00:00
dependabot[bot] 9bcb83a20a Bump smol-toml from 1.6.0 to 1.6.1 (#5249)
Bumps [smol-toml](https://github.com/squirrelchat/smol-toml) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/compare/v1.6.0...v1.6.1)

---
updated-dependencies:
- dependency-name: smol-toml
  dependency-version: 1.6.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:59:06 +00:00
dependabot[bot] dd8d8e5410 Bump brace-expansion from 1.1.12 to 1.1.13 (#5248)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:41:40 +00:00
dependabot[bot] 32b8ff8116 Bump minimatch from 3.1.2 to 3.1.5 (#5247)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:40:24 +00:00
Skye Elliot 3ceadd512d Refactor history sharing tests using setupClients helper (#5235)
* tests: Refactor history sharing tests using `setupClients` helper

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* tests: Use separate destructors for test clients

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-30 11:13:37 +00:00
renovate[bot] 8182180550 Update dependency happy-dom to v20.8.9 [SECURITY] (#5244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-30 09:09:17 +00:00
renovate[bot] aed74c5a72 Update dependency happy-dom to v20.8.8 [SECURITY] (#5243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-27 20:09:41 +00:00
renovate[bot] 8c259c53a6 Update actions/deploy-pages action to v5 (#5238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 20:06:08 +00:00
Michael Telatynski 4d59291538 Disable Complement Crypto tests (#5242)
Disabled Complement Crypto tests due to flakiness.
2026-03-27 17:38:37 +00:00
Michael Telatynski 80009a1b31 Add support for non-root package.json for version calculation in Sonar workflow (#5240)
* Add support for non-root package.json for version calculation in Sonar workflow

* Fix version_cmd
2026-03-27 09:16:01 +00:00
renovate[bot] 93e6c95953 Update dependency typescript to v6 (#5234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 03:16:33 +00:00
renovate[bot] 27a5507cef Update dependency knip to v6 (#5237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 02:41:41 +00:00
renovate[bot] be06f6655e Update dependency typedoc to v0.28.18 (#5236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 22:56:30 +00:00
Skye Elliot 71152f33bf Rotate the current room key when we see a member leave (#5231)
* feat: Rotate room key when a member leaves the room

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* test: Assert room key rotated to prevent MSC4268 leaking keys

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* docs: Outline key rotation scenario above discard logic

* feat: Use `RoomStateEvents.Events` over membership event

* docs: Correct spelling in scenario explanation

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

* docs: Pull scenario explanation up to `onRoomStateEvent`

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* tests: Assert room key is rotated under leave va gappy sync

* tests: Build sync response incrementally for gappy/ungappy sync

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-25 16:39:58 +00:00
RiotRobot bc8f67089c Merge branch 'master' into develop 2026-03-24 11:22:32 +00:00
RiotRobot acc9aa8939 v41.2.0 2026-03-24 11:21:57 +00:00
Richard van der Hoff e76f627fe3 Add some docs to the DeviceIsolationModes (#5232)
* Add some docs to the DeviceIsolationModes

Notes to help us/me remember how these relate to MSC4153.

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2026-03-23 15:29:35 +00:00
renovate[bot] 45b1e73842 Update npm non-major dependencies (#5229)
* Update npm non-major dependencies

* Update test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-20 09:58:04 +00:00
renovate[bot] f3eefd2f32 Update element-hq/element-meta digest to 7f2f93f (#5222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 09:27:19 +00:00
renovate[bot] f7c053216b Update dependency eslint-plugin-jsdoc to v62.8.0 (#5228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 09:18:31 +00:00
renovate[bot] 9c7739f14f Update actions/download-artifact digest to 3e5f45b (#5220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 09:10:38 +00:00
renovate[bot] 897afe153a Update pnpm/action-setup digest to fc06bc1 (#5225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 19:03:49 +00:00
renovate[bot] a929391dcd Update shogo82148/actions-upload-release-asset digest to 96bc1f0 (#5226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 18:06:53 +00:00
renovate[bot] 45c5ee9f65 Update actions/setup-node digest to 53b8394 (#5221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 16:11:41 +00:00
renovate[bot] e56aaa16c7 Update mheap/github-action-required-labels digest to 0ac283b (#5224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 15:47:21 +00:00
renovate[bot] da0d3d791e Update element-hq/element-web digest to 9730933 (#5223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 15:04:32 +00:00
renovate[bot] ed5eb670a1 Update zizmorcore/zizmor-action action to v0.5.2 (#5227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 14:27:14 +00:00
renovate[bot] b7fcb6e4c1 Update pnpm to v10.32.1 (#5230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 14:19:06 +00:00
RiotRobot bd775f6b61 v41.2.0-rc.0 2026-03-17 11:39:31 +00:00
Skye Elliot c2f9ad28fc Only share history if room history visibility is shared (#5216)
* feat: Only share history if room history visibility is shared

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* docs: Update documentation for `InviteOpts.shareEncryptedHistory`

* tests: Ensure shared history respects current history visibility

This commit additionally modifies `expectSendRoomEvent` to remove
the matcher on success, since fetchmock takes a while to do this
automatically.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-16 13:03:41 +00:00
Richard van der Hoff 7f33e3462e History sharing: resume key-bundle import on restart (#5214)
* Store rooms pending key bundles in the CryptoStore

Replace the in-memory storage of which rooms are waiting for a key bundle with
permanent storage in the crypto store.

* Clear pending-key-bundle flag on malformed bundles

If we cannot import the key bundle, there is no point trying again another
time: we may as well clear the flag either way.

* Factor out some helpers in history sharing integ test

* Do not accept key bundles for rooms we joined more than 24h ago

Per discussion in crypto-internal.

* Clear pending key bundle data when we leave a room

* Resume key-bundle import on restart

* Clear pending-key-bundle flag on rooms that we joined ages ago

* fixup! Clear pending-key-bundle flag on malformed bundles
2026-03-16 11:58:36 +00:00
Richard van der Hoff d99363d288 Move shareRoomHistoryWithUser to CryptoBackend (#5218)
There is no need for this method to be exposed to the application, and it's a
footgun waiting to trap the unwary user.

It's marked `@experimental` so we're allowed to move it without a major version
bump.
2026-03-12 21:21:31 +00:00
renovate[bot] 3642b99212 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v18 (#5217)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v18

* Adapt to breaking changes in rust-sdk wasm bindings

* more types fixes

* types fixes for tests

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2026-03-12 18:16:44 +00:00
Valere Fedronic 6ec0987286 re export sticky event types (#5213)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2026-03-12 17:27:11 +00:00
RiotRobot 219eb617dc Merge branch 'master' into develop 2026-03-10 13:46:12 +00:00
RiotRobot c6f9b25046 v41.1.0 2026-03-10 13:45:36 +00:00
Michael Telatynski 5d0e2efaf3 Add zizmor CI & make it happy (#5212)
* Add zizmor CI & make it happy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix additional zizmor warning

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-04 09:27:47 +00:00
renovate[bot] c7cd5570d3 Update eslint-plugins (#5207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 14:01:28 +00:00
renovate[bot] c2f6dd2ce0 Update crazy-max/ghaction-import-gpg action to v7 (#5210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:24:07 +00:00
renovate[bot] 393732aaae Update npm non-major dependencies (#5208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:23:47 +00:00
renovate[bot] d373fd8540 Update GitHub Artifact Actions (#5211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:22:31 +00:00
renovate[bot] 44a8a9a47a Update pnpm to v10.30.3 (#5209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:22:19 +00:00
RiotRobot 8a0b7ad68b v41.1.0-rc.0 2026-03-03 13:12:49 +00:00
Andy Balaam 09663302e1 Throw a specific error when the backup decryption key does not match the public backup (#5202) 2026-03-02 13:57:25 +00:00
dependabot[bot] 94f83b702c Bump rollup from 4.57.1 to 4.59.0 (#5203)
Bumps [rollup](https://github.com/rollup/rollup) from 4.57.1 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.57.1...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 13:48:20 +00:00
Michael Telatynski 9df27ee672 Fix downstream tsc (#5204)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-02 13:29:42 +00:00
Michael Telatynski 5739b59faa Update release workflows to deal with monorepos (#5201)
* Update release workflows to deal with monorepos

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Do the same for release-gitflow.yml

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-25 08:58:06 +00:00
Valere Fedronic e4425570c7 cleanup: Remove deprecated rtc room key transport (#5119)
* cleanup: Remove deprecated rtc room key transport

* fix: rtc statistics are managed by transport directly

* mark as readonly

* cleanup do not use deprecated `room`

* doc: Add missing param doc

* fixup: add back test wrongly removed
2026-02-24 13:32:08 +00:00
RiotRobot 145cb26054 Merge branch 'master' into develop 2026-02-24 13:59:12 +00:00
RiotRobot 26d5b1cde2 v41.0.0 2026-02-24 13:58:36 +00:00
renovate[bot] 0666d6b4e1 Update dependency typedoc to v0.28.17 (#5196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 11:23:34 +00:00
Michael Telatynski 9002064f10 Remove rimraf in favour of --delete-dir-on-start (#5200)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-24 11:01:56 +00:00
Will Hunt 5ea1554612 RTC Slots: Refactoring of membership event parsing and handling (#5134)
* Split out membership into seperate files.

* First pass of merging in new changes.

* More cleanup

* fix import

* Lots of test fixes

* remove skips

* unrelated change

* docstring

* comment

* lint lint lint

* copyright updates

* cleanup

* Ensure we await initial membership in all tests.

* fix race

* Use promises which are more reliable

* Even more promise stability

* cleanup

* Cleanup

* rename legacy.ts -> session.ts

* Update imports

* cleanup

* Rename files

* Rename + remove claimed_

* renaming

* Rename function

* All the cleanup

* tidy

* commit changes

* fix call membership

* fix claimed

* update slot_id

* fix device_id / claimed_device_id

* Update src/matrixrtc/utils.ts

Co-authored-by: R Midhun Suresh <hi@midhun.dev>

* use an aggregate error

* Export types

---------

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
2026-02-24 10:51:23 +00:00
Will Hunt bd6547c081 Update getUrlPreview to use /_matrix/client/v1/media/preview_url (#5191)
* Update preview_url endpoint.

* support <v1.11

* Add tests
2026-02-23 19:49:05 +00:00
Michael Telatynski 8073f27d98 Specify this type for BaseLogger methods (#5198)
This allows https://typescript-eslint.io/rules/unbound-method/ to be happy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-19 23:05:15 +00:00
renovate[bot] 3bb22a9b28 Update npm non-major dependencies (#5197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 11:13:22 +00:00
renovate[bot] ba8bb3228d Update dependency eslint-plugin-jsdoc to v62.5.4 (#5195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 17:18:07 +00:00
renovate[bot] de23c9587b Update actions/checkout digest to de0fac2 (#5193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 17:17:30 +00:00
renovate[bot] 64eb482a49 Update actions/stale digest to b5d41d4 (#5194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 16:17:31 +00:00
RiotRobot aba7f8a0d4 v41.0.0-rc.0 2026-02-17 15:11:09 +00:00
RiotRobot 5495153c63 Merge remote-tracking branch 'origin/develop' into staging 2026-02-17 14:43:42 +00:00
Michael Telatynski 4f0696e2a4 Revert "Disable complement crypto tests temporarily (#5188)" (#5190)
This reverts commit 327d2fa7c8.
2026-02-17 11:13:05 +00:00
Bas Nijholt 0e659d294e fix(relations): prevent stale m.replace from overriding newer edits (#5192)
When multiple m.replace edits arrive concurrently, getLastReplacement()
may block on decryption. If an older edit's decryption completes after a
newer edit has already been applied, the older async result overwrites
the target event with stale content.

Add a monotonic update counter (replacementUpdateId) and centralise all
replacement updates through updateTargetEventReplacement(). The method
captures the counter before awaiting and discards the result if a newer
update has started in the meantime.

This race is especially pronounced in encrypted rooms with rapid
streaming-style edits, where variable decryption timing causes
out-of-order promise resolution.
2026-02-17 09:47:08 +00:00
Skye Elliot e74eb4928e Download room keys from backup prior to buliding historic room key bundles (#5171)
* chore: Update `@matrix-org/matrix-sdk-crypto-wasm` to v17.1.0

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* feat: Download keys from key backup.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* tests: Ensure backup is downloaded before building room key bundle.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* fix: Address review comments in history sharing tests.

* docs: Improve `getSyncResponse` and `assertInviteAndShareHistory` docs

* feat: Log `backupVersion`, `hasDecryptionKey` on download failure

* fix: Group backup data, add casts to `generate-test-data.py`.

* tests: Update `getSyncResponse` calls in history sharing integ tests

* fix: Add history visibility argument to state events test.

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-02-13 13:31:14 +00:00
David Baker 327d2fa7c8 Disable complement crypto tests temporarily (#5188)
* Disable complement crypto tests temporarily

As per comment

* and the downstream one too

* Stub downstream test instead

* prettier
2026-02-12 14:57:34 +00:00
Aditya Cherukuru 028357f15f Fix reactive display name disambiguation (#5135)
* Fix reactive display name disambiguation

When a room member changes their display name, recalculate the disambiguation flag for all other members who share (or previously shared) that display name. This ensures that the 'disambiguate' flag is updated reactively when display name conflicts appear or are resolved.

Fixes element-hq/element-web#468

Fixes element-hq/element-web#4795

Fixes element-hq/element-web#31551

Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com>

* Refactor: move disambiguation logic per review feedback

- Added updateDisambiguation() method to RoomMember for direct disambiguation recalculation

- Moved affected display name tracking to setStateEvents() instead of updateDisplayNameCache()

- Removed setMembershipEvent() hack, now calls updateDisambiguation() directly

Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com>

* Exclude processed members from disambiguation loop

Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com>

---------

Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com>
2026-02-12 14:23:04 +00:00
Michael Telatynski 872ec6755e Add support for Matrix Spec v1.13 (#5160) 2026-02-11 16:14:02 +00:00
RiotRobot 333d6a7bd6 v40.3.0-rc.0 2026-02-11 15:06:14 +00:00
Michael Telatynski 47532de452 Switch from yarn classic to pnpm (#5184) 2026-02-11 10:35:25 +00:00
RiotRobot 87e1049dae Merge branch 'master' into develop 2026-02-10 15:17:11 +00:00
RiotRobot 4c8e38009f v40.2.0 2026-02-10 15:16:24 +00:00
Timo 6e3efef0c5 Fix empty string to room compatibility trick to only apply to m.call (#5172)
* Fix empty string to room compatibility trick to only apply to m.call

* add logging

* fix linter

* Add tests

* limit logging.
2026-02-08 11:25:34 +00:00
Olivier 'reivilibre fb590627bb Add logging on MSC4108 DELETE request (#5140)
Just noticed these requests aren't logged,
which makes debugging difficult.
This is very drive-by, done in the web editor.

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
2026-02-05 10:13:44 +00:00
Michael Telatynski 24cc17c270 Merge remote-tracking branch 'origin/develop' into develop 2026-02-04 11:47:05 +00:00
Michael Telatynski 68084e8fc3 Fix vitest slow reporter crashing CI
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-04 11:46:48 +00:00
Richard van der Hoff 0c3bb1f246 Add m.invite_permission_config account data type (#5183)
For MSC4380
2026-02-04 11:00:16 +00:00
renovate[bot] 7f42b67f68 Update matrix-org (#5181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 15:11:44 +00:00
renovate[bot] 9b871ac969 Update dependency eslint-plugin-jsdoc to v62.5.0 (#5179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:39:51 +00:00
renovate[bot] 49f7972a9e Update peter-evans/repository-dispatch digest to 28959ce (#5177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:16:42 +00:00
renovate[bot] c5ae4c8c0d Update dependency typedoc-plugin-mdn-links to v5.1.1 (#5180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:02:42 +00:00
renovate[bot] 2423300acd Update npm non-major dependencies (#5178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 14:01:03 +00:00
renovate[bot] 6cafa175b8 Update guibranco/github-status-action-v2 digest to 9bfa877 (#5176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 13:55:10 +00:00
RiotRobot 40165942e3 v40.2.0-rc.0 2026-02-03 13:14:44 +00:00
Hugh Nimmo-Smith f301251ff5 Clean up the ValidatedAuthMetadata types (#5175)
We don't expect oidc-client-ts to provide the `device_authorization_endpoint` in the `OidcMetadata` because it isn't part of the OIDC spec.

As such, I think it makes sense to standardise on defining the metadata fields in `validate.ts` and clarify where they come from.
2026-02-02 17:27:33 +00:00
Hugh Nimmo-Smith 21cd5e98c1 Use stable /auth_metadata endpoint where homeserver supports v1.15 (#5174) 2026-02-02 15:25:48 +00:00
Andy Balaam db070dca57 Support additional_creators in upgradeRoom (MSC4289) (#5173)
* Support additional_creators in upgradeRoom (MSC4289)

Signed-off-by: Andy Balaam <andy.balaam@matrix.org>

* Remove unneeded undefined in type definition

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

---------

Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-02-02 11:24:53 +00:00
Timo 8b6ff0abcb [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated (#5168)
* Make sure we do not call this.syncApi.injectRoomEvents before the
syncApi is instanciated

* sonarCube remove complexity (embedded.js constructor)
2026-01-29 16:22:41 +00:00
Timo f2157f28bb [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId (#5166)
* Minimal change to transition from "" to "ROOM" as the callId/slotId

* Also transition MembershipManager tests to `"ROOM"`

* fix merge
2026-01-29 15:37:34 +00:00
Timo 739b8e1f89 Remove sending of deprecated notify event (we now use (#5167)
`m.rtc.notification`
2026-01-27 15:52:41 +00:00
Timo bc57a3f829 [MatrixRTC] Do not sent the livekit_alias in sticky events (#5165)
* Do not sent the livekit_alias in sticky events

* tests
2026-01-27 15:23:14 +00:00
RiotRobot f136f6ddf7 Merge branch 'master' into develop 2026-01-27 12:39:59 +00:00
RiotRobot d428e7119a v40.1.0 2026-01-27 12:39:19 +00:00
ElementRobot 5532066178 Recalculate room name on loading members (#5158) (#5164)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-27 12:33:09 +00:00
R Midhun Suresh 82b51d0d46 Improve startup performance by using promise.all when processing rooms from sync (#5095)
* Use Promise.all when processing rooms in sync response

* Remove import
2026-01-27 08:56:26 +00:00
David Baker fb12a5a1d6 Recalculate room name on loading members (#5158)
* Recalculate room name on loading members

Because if it's a DM room, loading members might change the room name

* Swap other userA / userB constants

* Typo

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

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-26 12:25:41 +00:00
Hugh Nimmo-Smith 61ea5a7dfc Add OAuthGrantType enum for OAuth 2.0 API grant types (#5161)
* Add OAuthGrantType enum for OAuth 2.0 API grant types

* Resolve circular imports

* Fix complication error
2026-01-23 13:46:56 +00:00
Hugh Nimmo-Smith 4e032317fe Add support for stable OAuth2.0 aware feature from MSC3824 (#5159)
* Add support for stable OAuth2.0 aware feature from MSC3824

* Use stable name internally

* Mark DELEGATED_OIDC_COMPATIBILITY as

* Add tsdoc config for @alias JSDoc modifier
2026-01-23 09:50:12 +00:00
Robin dbb2ae5c07 Give RoomWidgetClient the ability to send and receive sticky events (#5142)
* Give RoomWidgetClient the ability to send and receive sticky events

* linter

* Fix existing tests

* Add tests for sticky event support in embedded clients

* Update sticky event widget capability identifiers

In matrix-widget-api 0.16.1 they are updated to use the new unstable prefix from MSC4407.

* Explicitly require matrix-widget-api ≥ 1.16.1

* remove TODO comment

* simplify type lint checks
This is needed for EW donwstream tests. Otherwise it will through:
Error: matrix-js-sdk/src/embedded.ts(417,21): error TS2345: Argument of
type 'string | number | boolean | string[]' is not assignable to
parameter of type 'number'.

---------

Co-authored-by: Timo K <toger5@hotmail.de>
2026-01-22 16:07:13 +00:00
Michael Telatynski c8032a214e Typescript fixes (#5157)
* Remove deprecated tsconfig baseUrl

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix imports in tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix duplicated map entry

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-21 17:28:10 +00:00
Timo 6e34ca6f2d [MatrixRTC] Fix delayId not resetting on leave (#5156)
* reset delay id to undefined after sucessfully sending it.

* add tests

* setAndEmitDelayId signature
dont allow implicit undefined
2026-01-21 13:53:55 +00:00
Andy Balaam 4a7a699623 Unit tests for OutgoingRequestsManager not repeating failed requests (#5154) 2026-01-21 12:03:09 +00:00
renovate[bot] 774776178e Update npm non-major dependencies (#5150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 15:08:00 +00:00
renovate[bot] ed4078528d Update dependency typedoc to v0.28.16 (#5149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 15:03:52 +00:00
renovate[bot] 7224961e9e Update actions/setup-node digest to 6044e13 (#5147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:44:56 +00:00
renovate[bot] 35ca07e8fb Update matrix-org/sonarcloud-workflow-action digest to ea0cd9d (#5148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:44:47 +00:00
renovate[bot] ccffb5df2d Update dependency eslint-plugin-jsdoc to v62 (#5153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:44:04 +00:00
renovate[bot] 0d162f66f3 Update dependency matrix-widget-api to v1.16.0 (#5152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 14:43:47 +00:00
RiotRobot bb7a689448 v40.1.0-rc.0 2026-01-20 13:57:24 +00:00
Andy Balaam c2b464a72c Avoid rapidly retrying failed requests (#5146)
After https://github.com/matrix-org/matrix-js-sdk/pull/5109 we retry
failed requests in a tight loop, instead of once every sync. When
requests are consistently failing, e.g. when /keys/uploads is failing
because of a duplicate OTK, this causes us to make many requests,
causing load on the server.

The fix is to reprocess the outgoing requests loop only if at least one
request succeeded in the last batch.

Fixes https://github.com/element-hq/element-web/issues/31790
2026-01-20 13:14:51 +00:00
Timo 4a75d2c92f [matrixRTC] MatrixRTCSessions, add missing event reemission. (#5144)
* add missing event reemission.

* review

* CI (SonarCube Code quality)
2026-01-19 10:49:37 +00:00
Michael Telatynski 899cdb0e1d Switch from Jest to Vitest (#5131)
* Skip unwritten tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy jest fake timers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary sessionStorage mock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve async assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve error assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve object assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove assertion testing unclear mock

This test failed when ran individually, same as after the clearAllMocks call

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Avoid awaiting non-thenables

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Pass nop function when stubbing out console, vitest won't accept it any other way

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary mock which causes tests to fail after updating fetch-mock & fix typo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix mistaken assertions not testing all values in array

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix hidden non-running tests in room.spec.ts

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update fetch-mock-jest to @fetch-mock/jest

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make knip happier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make knip happier 2.0

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch from Jest to Vitest

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix CI

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary fake timers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update vite

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert irrelevant changes

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix coverage spec paths

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix slow test reporter

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix bad merge conflict resolution

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix babel config

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-15 11:15:37 +00:00
Michael Telatynski da7c6717fe Update fetch-mock-jest to @fetch-mock/jest (#5136)
* Skip unwritten tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy jest fake timers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary sessionStorage mock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve async assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve error assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve object assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove assertion testing unclear mock

This test failed when ran individually, same as after the clearAllMocks call

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Avoid awaiting non-thenables

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Pass nop function when stubbing out console, vitest won't accept it any other way

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary mock which causes tests to fail after updating fetch-mock & fix typo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix mistaken assertions not testing all values in array

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix hidden non-running tests in room.spec.ts

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update fetch-mock-jest to @fetch-mock/jest

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make knip happier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make knip happier 2.0

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-14 12:48:23 +00:00
Michael Telatynski b3fedf3a4e Prepare for jest->vitest (#5137)
* Skip unwritten tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy jest fake timers

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary sessionStorage mock

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve async assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve error assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve object assertions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove assertion testing unclear mock

This test failed when ran individually, same as after the clearAllMocks call

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Avoid awaiting non-thenables

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Pass nop function when stubbing out console, vitest won't accept it any other way

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unnecessary mock which causes tests to fail after updating fetch-mock & fix typo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix mistaken assertions not testing all values in array

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix hidden non-running tests in room.spec.ts

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-13 15:19:50 +00:00
RiotRobot 3d0ebdf6f1 Merge branch 'master' into develop 2026-01-13 14:28:56 +00:00
RiotRobot 25555ec431 v40.0.0 2026-01-13 14:28:14 +00:00
Hugh Nimmo-Smith 33cd424f1f Add stable m.oauth UIA stage enum (#5138) 2026-01-13 08:37:23 +00:00
Robin 4d0d32307e Use normal base64 encoding for RTC backend identities (#5129)
* Use normal base64 encoding for RTC backend identities

MSC4195 has been updated to specify that normal (non-URL-safe) base64 is the correct encoding for LiveKit participant identities.

* Test RTC backend identity computation
2026-01-09 17:28:42 +00:00
Will Hunt b1a578f62e export parseCallNotificationContent and isMyMembership from types (#5132) 2026-01-09 16:30:17 +00:00
renovate[bot] 841b654c00 Update typescript (#4951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-08 18:56:40 +00:00
Richard van der Hoff ca0d4622b3 Deprecate unused EventShieldReason reason codes (#5127) 2026-01-08 18:14:33 +00:00
Richard van der Hoff bfd87a0896 Add MatrixEvent.getKeyForwardingUser (#5128)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v17

* Remove references to `ShieldStateCode.SentInClear`

This was never used, and is no longer exported, by rust-sdk-crypto-wasm, so we
need to remove references to it.

* Add `MatrixEvent.getKeyForwardingUser`

Expose information about keys forwarded via MSC4286, via a new method on
`MatrixEvent`.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 16:25:18 +00:00
renovate[bot] 6c59b0c22f Update dependency @matrix-org/matrix-sdk-crypto-wasm to v17 (#5126)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v17

* Remove references to `ShieldStateCode.SentInClear`

This was never used, and is no longer exported, by rust-sdk-crypto-wasm, so we
need to remove references to it.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2026-01-08 12:57:22 +00:00
Travis Ralston eff75c6525 Add types for (unstable) policy servers (#5116) 2026-01-08 00:19:57 +00:00
Valere Fedronic ee4a0b001e MatrixRTC: Cleaning up + address some sonarqube issues (#5125)
* cleanup: Remove deprecated API

* clean: breakdown method to reduce cognitive complexity

* cleanup: use readonly has never reassigned

* cleanup: Do not use an object literal as default

* quick format

* fixup: missed a param while refactoring

* cleanup: additional breakdown to reduce cognitive complexity

* review: better names
2026-01-07 13:20:54 +00:00
renovate[bot] 5fcd6fd744 Update npm non-major dependencies (#5123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 15:18:24 +00:00
renovate[bot] bf58f18b5f Update GitHub Artifact Actions (#5124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 14:30:16 +00:00
renovate[bot] 0c020b3ca4 Update dependency eslint-plugin-jest to v29.12.1 (#5122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 14:30:02 +00:00
renovate[bot] 2727ebb67f Update shogo82148/actions-upload-release-asset digest to 8f6863c (#5121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 14:29:53 +00:00
renovate[bot] 8fae4f3111 Update matrix-org/sonarcloud-workflow-action digest to 9f6f057 (#5120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 14:29:30 +00:00
RiotRobot 455b614008 v40.0.0-rc.0 2026-01-06 14:01:30 +00:00
Will Hunt 93f4f40202 Implement MSC4387: M_SAFETY error (#5107)
* Implement MatrixSafetyError

* Mention safety error on associated functions.

* fix import

* move error

* cleanup and add test

* wording

* fix test

* fixup error

* Fix exp
2026-01-06 13:04:52 +00:00
Richard van der Hoff aeade9ce58 Remove unused property MatrixEvent.untrusted (#5118)
* Remove unused property MatrixEvent.untrusted

This was never set to anything other than `false`. I think it is a hangover
from pre-rust-sdk.

* Remove call to redundant `isKeySourceUntrusted`

`isKeySourceUntrusted` always returns false so no point calling it

* Remove dangling assignments to MatrixEvent.untrusted
2026-01-06 11:23:13 +00:00
Timo 4b89fb23c5 MatrixRTC Pseudonymous livekit identities (#5110)
* deprecate membershipID -> memberId & memberId -> stateKey in membership
manager

The membership manager used the memberId label for the stateKey. But
only the StickymembershipManager really has a configurable memberId.

* participantId -> callMembershipIdentityParts

The participantId is a termonology from livekit. We do not want it in
here! We want the js-sdk to be mostly transport agnostic. We do the
transition from the identity parts to the acutal livekit identity in
Element call (`sha256(userId+deviceId+memberId)`)

* update tests

* Expose `kind` to decide if we use the hashed or non hashed livekit
participants.

* expose delayId from the matrixRTCSession for delayed event delegation.

* rename if to mapKey

* backandId computation as part of the js-sdk

* review valere

* valr + timo keysWithoutMatchingRTCMembership

* fix legacy encryption manager

* fix doc issue

* fix doc

* fix imports

* Encryption Manager needs own rtcBackendIdentity to use

The encryption manager needs to signal our own key fast, cannot wait for remote echo of rtc membership. So it needs to be able to compute the rtcBackendIdentity

* fix test

* Remove double `useHashedRtcBackendIdentity` assignment. rename
variables.

* little improvements This stops the usage from the matrix event outside
the CallMemerbship constructor.

* fix logger import

* Add back deprecated API for compat

* Make change to CallMembership constructor backward compatible

* more backward compatible

---------

Co-authored-by: Valere <bill.carson@valrsoft.com>
2026-01-05 18:40:11 +00:00
Robin 174439c2f0 Make MatrixRTC encryption key types narrower for TS 5.9 compatibility (#5117)
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-9.html#libdts-changes

TypeScript 5.9 changes some things about the ArrayBuffer type and makes a number of DOM types, including the subtle crypto APIs, require a narrower buffer type as their input. For example if you wanted to use crypto.subtle.importKey to convert a MatrixRTC encryption key buffer given by matrix-js-sdk to a CryptoKey, you would run into a type error with TS 5.9. Specifying the type parameter of Uint8Array everywhere around the MatrixRTC files fixes this breakage.
2026-01-05 17:42:21 +00:00
Richard van der Hoff 43f3e10f05 Improve documentation on rawDisplayName (#5114)
... since it's not actually the raw displayname, at all.
2025-12-18 14:57:14 +00:00
Andy Balaam 97fcdb2830 Make the enableEncryptedStateEvents property on MatrixClient public (#5113)
* Make the enableEncryptedStateEvents property on MatrixClient public

* fixup! Make the enableEncryptedStateEvents property on MatrixClient public

tsdoc for enableEncryptedStateEvents

* fixup! Make the enableEncryptedStateEvents property on MatrixClient public

Improve the description of enableEncryptedStateEvents
2025-12-18 13:57:20 +00:00
Andy Balaam 31e2d8eb20 Re-check outgoing requests after processing them (#5109)
... in case any new requests have been added during processing.
Fixes https://github.com/element-hq/element-web/issues/30988
2025-12-17 12:32:04 +00:00
Richard van der Hoff 633a5a8848 Mark forwardingCurve25519KeyChain as deprecated (#5111)
The Rust SDK always populates this as an empty array, so we may as well get rid
of it.
2025-12-16 14:05:26 +00:00
Richard van der Hoff a5086a09b9 Mark IEventDecryptionResult as deprecated (#5112)
This is supposed to be js-sdk-internal
2025-12-16 14:05:16 +00:00
RiotRobot c251be9ae5 Merge branch 'master' into develop 2025-12-16 13:51:14 +00:00
RiotRobot ec137cb5fb v39.4.0 2025-12-16 13:50:28 +00:00
David Baker ab4e24f115 Make token refresher init itself lazily (#5106)
* Make token refresher init itself lazily

It needs a network connection to do the init, so this would fail if
a client tried to do it at startup with no internet, causing the token
to just never be refreshed.

This just changes the API (compatibly) to do the init lazily.

The promise is kept is retain backwards compat, it can be removed
later.

* Make deviceId protected

* Fix tests
2025-12-12 18:23:43 +00:00
Will Hunt 2218ec4e31 Add _unstable_getRTCTransports to client. (#5104) 2025-12-11 17:35:54 +00:00
Michael Telatynski 319a8309c5 Update SonarCloud workflow action version 2025-12-11 16:35:10 +00:00
Timo 5af046f54f Use membershipID for session events (#5105)
* User membershipID for session events

* fix tests
2025-12-11 12:40:47 +00:00
Timo f97a9d9762 Remove three status cases that will never be set. (#5103) 2025-12-10 17:47:10 +00:00
renovate[bot] dc1a57a9f2 Update typescript-eslint monorepo to v8.48.1 (#5100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 17:52:39 +00:00
renovate[bot] 2d6111a04b Update actions/setup-node digest to 395ad32 (#5097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 16:14:08 +00:00
renovate[bot] 710fd7859d Update dependency eslint-plugin-jsdoc to v61.5.0 (#5101)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:15:42 +00:00
renovate[bot] e340a4ceaf Update dependency @matrix-org/matrix-sdk-crypto-wasm to v16 (#5102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:15:31 +00:00
renovate[bot] 3fa44e076e Update actions/stale digest to 9971854 (#5098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:13:49 +00:00
renovate[bot] 3f9fb9c936 Update matrix-org/sonarcloud-workflow-action digest to 820f7c2 (#5099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:13:35 +00:00
renovate[bot] 8db347a75e Update actions/checkout digest to 8e8c483 (#5096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:13:17 +00:00
renovate[bot] 8db3343280 Update dependency prettier to v3.7.0 (#5090)
* Update dependency prettier to v3.7.0

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-09 15:12:27 +00:00
renovate[bot] 25c5a5b4ff Update dependency @casualbot/jest-sonar-reporter to v2.5.0 (#5003)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 15:09:30 +00:00
RiotRobot a696e77652 v39.4.0-rc.0 2025-12-09 14:51:21 +00:00
Andy Balaam 582a76d87c Update encrypted state to say MSC4362 everywhere (#5079)
* Update encrypted state to say MSC4362 everywhere

* Fix test failure with encrypted state: handle empty string state key
2025-12-09 12:47:38 +00:00
Skye Elliot fdfddde55a Import room key bundles received after invite. (#5080)
* feat: Import room key bundles when received after invite.

* tests: Add spec test for room key bundle arriving after invite accepted.

* chore: Fix code quality issue (unnecessary async function).

* docs: Tidy up comments.

* refactor: Simplify key bundle importing after invite to one entrypoint.

- Remove `onReceiveToDeviceEvent` from `CryptoBackend`.
- Copy old room key bundle importing logic to
  `preprocessToDeviceEvents`.

* refactor: Move late bundle importing to main preprocess loop.

* fix: Use `Map` over `Record` to prevent prototype pollution.
2025-12-08 17:50:13 +00:00
Michael Telatynski 0ecfef2352 Update SonarCloud workflow action version 2025-12-04 13:22:19 +00:00
Michael Telatynski 4fdece6c1c Update SonarCloud action version in workflow 2025-12-04 13:14:18 +00:00
Michael Telatynski 6f0bce8708 Add label to skip Sonar coverage (#5094)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-04 12:22:38 +00:00
Michael Telatynski d3bdeb73f5 Avoid use of Optional type (#5093)
* Avoid use of Optional type

As we are likely to remove dependency on matrix-events-sdk

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tweak params

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-12-04 11:27:43 +00:00
RiotRobot 942fdf5bee Merge branch 'master' into develop 2025-12-02 14:45:10 +00:00
Will Hunt 3d1bcb73c1 Allow msc4354_sticky_key to be optional on sticky events. (#5073) 2025-11-27 11:54:49 +00:00
Michael Telatynski a960e686b3 Handle all response fields from /context API being optional (#5089)
* Handle all response fields from /context API being optional

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Simplify

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-27 11:12:39 +00:00
Michael Telatynski 946774c3fb Fix close-if-fork-develop job permissions (#5088)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-26 10:27:02 +00:00
renovate[bot] 15edbc8067 Update dependency @stylistic/eslint-plugin to v5.6.1 (#5083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 15:48:46 +00:00
renovate[bot] 1398ac24a2 Update typescript-eslint monorepo to v8.47.0 (#5086)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 15:48:38 +00:00
renovate[bot] c76df4cd8f Update all non-major dependencies (#5082)
* Update all non-major dependencies

* Make knip happy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-25 15:43:23 +00:00
renovate[bot] a5e4dbf2d3 Update dependency matrix-widget-api to v1.15.0 (#5084)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 14:58:14 +00:00
renovate[bot] 3768187395 Update eslint-plugins (#5085)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 14:58:04 +00:00
renovate[bot] 08d0ce25f1 Update actions/checkout action to v6 (#5087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 14:57:19 +00:00
246 changed files with 17753 additions and 14163 deletions
+28 -12
View File
@@ -1,6 +1,6 @@
module.exports = {
plugins: ["matrix-org", "import", "jsdoc", "n"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
plugins: ["matrix-org", "import", "jsdoc", "n", "@vitest"],
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
parserOptions: {
project: ["./tsconfig.json"],
},
@@ -83,16 +83,6 @@ 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",
// Used in some crypto tests.
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
},
],
},
overrides: [
{
@@ -147,11 +137,37 @@ module.exports = {
},
{
files: ["spec/**/*.ts"],
extends: ["plugin:@vitest/legacy-recommended"],
rules: {
// We don't need super strict typing in test utilities
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-empty-object-type": "off",
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"@vitest/no-disabled-tests": "off",
// Used in some crypto tests.
"@vitest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
},
],
"@vitest/expect-expect": [
"error",
{
assertFunctionNames: [
"expect",
"expectDevices",
"assert.isTrue",
"assert.isFalse",
"passwordTest",
"compareHeaders",
"doTest",
],
},
],
},
},
{
+1 -1
View File
@@ -1,7 +1,7 @@
* @matrix-org/element-web-reviewers
/.github/workflows/** @matrix-org/element-web-team
/package.json @matrix-org/element-web-team
/yarn.lock @matrix-org/element-web-team
/pnpm-lock.yaml @matrix-org/element-web-team
/scripts/** @matrix-org/element-web-team
/src/webrtc @matrix-org/element-call-reviewers
/src/matrixrtc @matrix-org/element-call-reviewers
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@59cbc563d11314e48122193f8fe5cdda62ea6cf9 # v1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@59cbc563d11314e48122193f8fe5cdda62ea6cf9 # v1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@59cbc563d11314e48122193f8fe5cdda62ea6cf9 # v1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+3
View File
@@ -41,3 +41,6 @@
- name: "Z-Flaky-Test"
description: "A test is raising false alarms"
color: "ededed"
- name: "Z-Skip-Coverage"
description: "Skip SonarQube coverage for this PR"
color: "ededed"
+3 -1
View File
@@ -1,6 +1,8 @@
name: Backport
on:
pull_request_target:
# Privilege escalation necessary to enable backporting PRs from forks
# 🚨 We must not execute any checked out code here.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- closed
- labeled
+4 -2
View File
@@ -1,7 +1,9 @@
name: Deploy documentation PR preview
on:
workflow_run:
# Privilege escalation necessary to publish to Netlify
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["Static Analysis"]
types:
- completed
@@ -15,7 +17,7 @@ jobs:
deployments: write
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -1,7 +1,7 @@
# Triggers after the "Downstream artifacts" build has finished, to run the
# matrix-react-sdk playwright tests (with access to repo secrets)
# element-web playwright tests (with access to repo secrets)
name: matrix-react-sdk End to End Tests
name: Element Web End to End Tests
on:
merge_group:
types: [checks_requested]
@@ -21,11 +21,12 @@ concurrency:
jobs:
playwright:
name: Playwright
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
uses: element-hq/element-web/.github/workflows/build-and-test.yaml@develop # zizmor: ignore[unpinned-uses]
permissions:
actions: read
issues: read
pull-requests: read
contents: read
with:
matrix-js-sdk-sha: ${{ github.sha }}
# We only want to run the playwright tests on merge queue to prevent regressions
+2 -2
View File
@@ -18,8 +18,8 @@ jobs:
runs-on: ubuntu-24.04
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@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
- name: Notify element-web repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
+10 -5
View File
@@ -1,6 +1,9 @@
name: Pull Request
on:
pull_request_target:
# Privilege escalation necessary access members of the review teams
# 🚨 We must not execute any checked out code here, and be careful around use of user-controlled inputs.
# FIXME: only `community-prs` job needs this privilege, so it should be in its own workflow file.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [opened, edited, labeled, unlabeled, synchronize]
merge_group:
types: [checks_requested]
@@ -15,7 +18,7 @@ jobs:
name: Preview Changelog
runs-on: ubuntu-24.04
steps:
- uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
if: github.event_name != 'merge_group'
with:
labels: |
@@ -35,7 +38,7 @@ jobs:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
@@ -60,7 +63,7 @@ jobs:
- name: Add label
if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
github.rest.issues.addLabels({
@@ -73,13 +76,15 @@ jobs:
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-24.04
permissions:
pull-requests: write
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
github.rest.issues.createComment({
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check for X-Release-Blocker label on any open issues or PRs
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
REPO: ${{ inputs.repository }}
with:
@@ -16,18 +16,20 @@ jobs:
contents: write
steps:
- name: 🧮 Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: staging
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: package.json
cache: "yarn"
cache: "pnpm"
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
id: draft-release
@@ -37,7 +39,7 @@ jobs:
disable-autolabeler: true
- name: Get actions scripts
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
@@ -48,7 +50,7 @@ jobs:
- name: Ingest upstream changes
if: inputs.include-changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
draft:
permissions:
contents: write
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop # zizmor: ignore[unpinned-uses]
+13 -6
View File
@@ -12,20 +12,25 @@ on:
description: List of dependencies to reset.
type: string
required: false
dir:
description: The directory to release
type: string
default: "."
concurrency: ${{ github.workflow }}
permissions: {} # Uses ELEMENT_BOT_TOKEN
jobs:
merge:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
persist-credentials: true
- name: Get actions scripts
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
@@ -33,13 +38,14 @@ jobs:
sparse-checkout: |
scripts/release
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Set up git
run: |
@@ -53,6 +59,7 @@ jobs:
- name: Reset dependencies
if: inputs.dependencies
working-directory: ${{ inputs.dir }}
run: |
while IFS= read -r PACKAGE; do
[ -z "$PACKAGE" ] && continue
@@ -73,7 +80,7 @@ jobs:
fi
echo "Resetting $PACKAGE to develop branch..."
yarn add "github:matrix-org/$PACKAGE#develop"
pnpm add "github:matrix-org/$PACKAGE#develop"
git add -u
git commit -m "Reset $PACKAGE back to develop branch"
done <<< "$DEPENDENCIES"
+39 -19
View File
@@ -26,12 +26,21 @@ on:
description: |
The path to the asset you want to upload, if any. You can use glob patterns here.
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
Relative to `dir`.
type: string
required: false
expected-asset-count:
description: The number of expected assets, including signatures, excluding generated zip & tarball.
type: number
required: false
dist-dir:
description: The directory to release
type: string
default: "."
version-dirs:
description: Directories in which to update package.json `version` field
type: string
required: false
outputs:
npm-id:
description: "The npm package@version string we published"
@@ -43,7 +52,7 @@ jobs:
permissions:
issues: read
pull-requests: read
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop # zizmor: ignore[unpinned-uses]
release:
name: Release
@@ -56,7 +65,7 @@ jobs:
- name: Load GPG key
id: gpg
if: inputs.gpg-fingerprint
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
@@ -64,22 +73,23 @@ jobs:
- name: Get draft release
id: draft-release
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
env:
GITHUB_TOKEN: ${{ github.token }}
with:
draft: true
latest: true
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: staging
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
persist-credentials: true
- name: Get actions scripts
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
@@ -90,6 +100,7 @@ jobs:
- name: Prepare variables
id: prepare
working-directory: ${{ inputs.dist-dir }}
run: |
echo "VERSION=$VERSION" >> $GITHUB_ENV
@@ -104,7 +115,7 @@ jobs:
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
- name: Check version number not in use
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { VERSION } = process.env;
@@ -123,15 +134,17 @@ jobs:
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
node-version-file: package.json
cache: "pnpm"
node-version-file: ${{ inputs.dist-dir }}/package.json
- name: Install dependencies
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Handle develop dependencies
working-directory: ${{ inputs.dist-dir }}
run: |
ret=0
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
@@ -140,15 +153,19 @@ jobs:
VERSION=${dep[1]}
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
yarn upgrade "$PACKAGE@$VERSION" --exact
pnpm add "$PACKAGE@$VERSION" --save-exact
git add -u
git commit -m "Keep $PACKAGE at $VERSION"
done
- name: Bump package.json version
- name: Bump package.json versions
run: |
yarn version --no-git-tag-version --new-version "${VERSION#v}"
git add package.json
for DIR in $DIRS; do
pnpm version -C "$DIR" --no-git-tag-version "${VERSION#v}"
git add "$DIR"/package.json
done
env:
DIRS: ${{ inputs.version-dirs || inputs.dist-dir }}
- name: Add to CHANGELOG.md
if: inputs.final
@@ -175,7 +192,8 @@ jobs:
- name: Build assets
if: steps.prepare.outputs.has-dist-script == '1'
run: DIST_VERSION="$VERSION" yarn dist
working-directory: ${{ inputs.dist-dir }}
run: DIST_VERSION="$VERSION" pnpm dist
- name: Upload release assets & signatures
if: inputs.asset-path
@@ -183,7 +201,7 @@ jobs:
with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.draft-release.outputs.upload_url }}
asset-path: ${{ inputs.asset-path }}
asset-path: ${{ inputs.dist-dir }}/${{ inputs.asset-path }}
- name: Create signed tag
if: inputs.gpg-fingerprint
@@ -216,7 +234,7 @@ jobs:
- name: Validate release has expected assets
if: inputs.expected-asset-count
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
@@ -244,7 +262,7 @@ jobs:
git push origin master
- name: Publish release
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
FINAL: ${{ inputs.final }}
@@ -276,7 +294,9 @@ jobs:
name: Publish to npm
needs: release
if: inputs.npm
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop # zizmor: ignore[unpinned-uses]
with:
dir: ${{ inputs.dist-dir }}
permissions:
contents: read
id-token: write
+14 -6
View File
@@ -1,6 +1,11 @@
name: Publish to npm
on:
workflow_call:
inputs:
dir:
description: The directory to release
type: string
default: "."
outputs:
id:
description: "The npm package@version string we published"
@@ -17,26 +22,29 @@ jobs:
id: ${{ steps.npm-publish.outputs.id }}
steps:
- name: 🧮 Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: staging
persist-credentials: false
- name: 🔧 Yarn cache
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 pnpm cache
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
node-version-file: package.json
node-version-file: ${{ inputs.dir }}/package.json
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
working-directory: ${{ inputs.dir }}
run: |
npm publish --provenance --access public --tag "$TAG"
release=$(jq -r '"\(.name)@\(.version)"' package.json)
+24 -17
View File
@@ -24,7 +24,7 @@ concurrency: ${{ github.workflow }}
permissions: {} # No permissions required
jobs:
release:
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop # zizmor: ignore[unpinned-uses,secrets-inherit]
permissions:
contents: write
issues: write
@@ -41,28 +41,32 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
repo:
- element-hq/element-web
include:
- repo: element-hq/element-web
path: apps/web
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ matrix.repo }}
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
persist-credentials: true
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
node-version: "lts/*"
- name: Bump dependency
env:
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
DIR: ${{ matrix.path }}
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact
git add package.json yarn.lock
pnpm add -C "$DIR" "$DEPENDENCY" --save-exact
git add "$DIR"/package.json pnpm-lock.yaml
git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging
@@ -73,22 +77,25 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: 🧮 Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: 🔧 Yarn cache
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 pnpm cache
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: 📖 Generate docs
run: yarn gendoc
run: pnpm gendoc
- name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: _docs
@@ -106,4 +113,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
+14 -9
View File
@@ -12,7 +12,11 @@ on:
sharded:
type: boolean
required: false
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
version-pkg-json-dir:
type: string
default: "."
description: "Relative path of the directory containing package.json with the `version` to use."
permissions: {}
jobs:
sonarqube:
@@ -27,7 +31,7 @@ jobs:
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
- uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
@@ -36,14 +40,15 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
persist-credentials: false
- name: 📥 Download artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: ${{ !inputs.sharded }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -51,14 +56,13 @@ jobs:
name: coverage
path: coverage
- name: 📥 Download sharded artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: inputs.sharded
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: coverage-*
path: coverage
merge-multiple: true
- name: Check coverage artifact
run: |
if [ ! -d coverage ]; then
@@ -75,19 +79,20 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@820f7c2e9e94ba9e35add0f739691e5c7e23fa25 # v4.0
uses: matrix-org/sonarcloud-workflow-action@13968a27c924fa19b1dacbce6ca3ff217daa775b
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
skip_checkout: true
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
version_cmd: "cat package.json | jq -r .version"
skip_coverage_label: Z-Skip-Coverage
version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
- uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
+4 -2
View File
@@ -1,6 +1,8 @@
name: SonarQube
on:
workflow_run:
# Privilege escalation necessary to call upon SonarCloud
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["Tests"]
types:
- completed
@@ -16,7 +18,7 @@ jobs:
actions: read
statuses: write
id-token: write # sonar
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop # zizmor: ignore[unpinned-uses]
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+79 -53
View File
@@ -14,58 +14,61 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Typecheck
run: "yarn run lint:types"
run: "pnpm run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Run Linter
run: "yarn run lint:js"
run: "pnpm run lint:js"
node_example_lint:
name: "Node.js example"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Build Types
run: "yarn build:types"
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
cache: "npm"
node-version-file: "examples/node/package.json"
# cache-dependency-path: '**/package-lock.json'
run: "pnpm build:types"
- name: Install Example Deps
run: "npm install"
@@ -82,39 +85,50 @@ jobs:
workflow_lint:
name: "Workflow Lint"
runs-on: ubuntu-24.04
permissions:
security-events: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Run Linter
run: "yarn lint:workflows"
run: "pnpm lint:workflows"
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
docs:
name: "JSDoc Checker"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
run: "pnpm install"
- name: Generate Docs
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: docs
path: _docs
@@ -125,31 +139,36 @@ jobs:
name: "Analyse Dead Code"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
cache: "yarn"
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
run: "pnpm install --frozen-lockfile"
- name: Run linter
run: "yarn run lint:knip"
run: "pnpm run lint:knip"
element-web:
name: Downstream tsc element-web
if: github.event_name == 'merge_group'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: element-hq/element-web
persist-credentials: false
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
node-version: "lts/*"
- name: Install Dependencies
@@ -159,15 +178,22 @@ jobs:
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
- name: Typecheck
run: "yarn run lint:types"
working-directory: apps/web
run: "pnpm run lint:types"
# Hook for branch protection to skip downstream typechecking outside of merge queues
downstream:
name: Downstream Typescript Syntax Check
# Workflow consolidation job
done:
needs:
- ts_lint
- js_lint
- node_example_lint
- workflow_lint
- docs
- analyse_dead_code
- element-web
name: Static Analysis
runs-on: ubuntu-24.04
if: always()
needs:
- element-web
steps:
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
- if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
+1 -1
View File
@@ -11,7 +11,7 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
sync-labels:
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
with:
LABELS: |
element-hq/element-meta
+26 -24
View File
@@ -12,8 +12,8 @@ env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
permissions: {} # No permissions required
jobs:
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
test:
name: "Vitest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
@@ -22,17 +22,20 @@ jobs:
node: ["lts/*", 22]
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node
id: setupNode
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "yarn"
cache: "pnpm"
node-version: ${{ matrix.node }}
- name: Install dependencies
run: "yarn install"
run: "pnpm install"
- name: Get number of CPU cores
id: cpu-cores
@@ -40,24 +43,23 @@ jobs:
- name: Run tests
run: |
yarn test \
--coverage=${{ env.ENABLE_COVERAGE }} \
--ci \
--max-workers ${{ steps.cpu-cores.outputs.count }} \
pnpm test \
--coverage=${ENABLE_COVERAGE} \
--maxWorkers ${NUM_WORKERS} \
./spec/${{ matrix.specs }}
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
# tell jest to use coloured output
FORCE_COLOR: true
SHARD: ${{ matrix.specs }}
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
- name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true'
run: mv coverage/lcov.info coverage/${{ steps.setupNode.outputs.node-version }}-${{ matrix.specs }}.lcov.info
run: mv coverage/lcov.info coverage/${NODE_VERSION}-${{ matrix.specs }}.lcov.info
env:
NODE_VERSION: ${{ steps.setupNode.outputs.node-version }}
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
path: |
@@ -65,19 +67,19 @@ jobs:
!coverage/lcov-report
# Dummy completion job to simplify branch protections
jest-complete:
name: Jest tests
needs: jest
complete:
name: Tests
needs: test
if: always()
runs-on: ubuntu-24.04
steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
- if: needs.test.result != 'skipped' && needs.test.result != 'success'
run: exit 1
element-web:
name: Downstream test element-web
if: github.event_name == 'merge_group'
uses: element-hq/element-web/.github/workflows/tests.yml@develop
uses: element-hq/element-web/.github/workflows/tests.yml@develop # zizmor: ignore[unpinned-uses]
permissions:
statuses: write
with:
@@ -87,8 +89,8 @@ jobs:
complement-crypto:
name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group'
permissions: read-all
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
permissions: read-all # zizmor: ignore[excessive-permissions]
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
with:
use_js_sdk: "."
@@ -116,7 +118,7 @@ jobs:
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
automate-project-columns-next:
runs-on: ubuntu-24.04
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+1 -1
View File
@@ -6,6 +6,6 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
call-triage-labelled:
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@6339bcda15c71d209303b18a06a9b1c021220bf9
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
with:
operations-per-run: 250
days-before-issue-stale: -1
+1
View File
@@ -3,6 +3,7 @@
/.npmrc
/*.log
pnpm-lock.yaml
package-lock.json
.lock-wscript
build/Release
+124
View File
@@ -1,3 +1,127 @@
Changes in [41.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.3.0) (2026-04-07)
==================================================================================================
## 🐛 Bug Fixes
* Rotate the current room key when we see a member leave ([#5231](https://github.com/matrix-org/matrix-js-sdk/pull/5231)). Contributed by @kaylendog.
Changes in [41.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.2.0) (2026-03-24)
==================================================================================================
## ✨ Features
* Only share history if room history visibility is shared ([#5216](https://github.com/matrix-org/matrix-js-sdk/pull/5216)). Contributed by @kaylendog.
* History sharing: resume key-bundle import on restart ([#5214](https://github.com/matrix-org/matrix-js-sdk/pull/5214)). Contributed by @richvdh.
* Move `CryptoApi.shareRoomHistoryWithUser` to `CryptoBackend` ([#5218](https://github.com/matrix-org/matrix-js-sdk/pull/5218)). Contributed by @richvdh.
Changes in [41.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.1.0) (2026-03-10)
==================================================================================================
## ✨ Features
* Throw a specific error when the backup decryption key does not match the public backup ([#5202](https://github.com/matrix-org/matrix-js-sdk/pull/5202)). Contributed by @andybalaam.
* Update getUrlPreview to use /\_matrix/client/v1/media/preview\_url ([#5191](https://github.com/matrix-org/matrix-js-sdk/pull/5191)). Contributed by @Half-Shot.
Changes in [41.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.0.0) (2026-02-24)
==================================================================================================
## 🚨 BREAKING CHANGES
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
## ✨ Features
* Download room keys from backup prior to buliding historic room key bundles ([#5171](https://github.com/matrix-org/matrix-js-sdk/pull/5171)). Contributed by @kaylendog.
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
* Add logging on MSC4108 DELETE request ([#5140](https://github.com/matrix-org/matrix-js-sdk/pull/5140)). Contributed by @reivilibre.
* Add `m.invite_permission_config` account data type ([#5183](https://github.com/matrix-org/matrix-js-sdk/pull/5183)). Contributed by @richvdh.
## 🐛 Bug Fixes
* fix(relations): prevent stale m.replace from overriding newer edits ([#5192](https://github.com/matrix-org/matrix-js-sdk/pull/5192)). Contributed by @basnijholt.
* Fix reactive display name disambiguation ([#5135](https://github.com/matrix-org/matrix-js-sdk/pull/5135)). Contributed by @aditya-cherukuru.
* Fix empty string to room compatibility trick to only apply to m.call ([#5172](https://github.com/matrix-org/matrix-js-sdk/pull/5172)). Contributed by @toger5.
Changes in [40.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.2.0) (2026-02-10)
==================================================================================================
## 🦖 Deprecations
* [MatrixRTC] Remove sending of deprecated `notify` event (we now use `m.rtc.notification`) ([#5167](https://github.com/matrix-org/matrix-js-sdk/pull/5167)). Contributed by @toger5.
## ✨ Features
* Use stable /auth\_metadata endpoint where homeserver supports v1.15 ([#5174](https://github.com/matrix-org/matrix-js-sdk/pull/5174)). Contributed by @hughns.
* Support additional\_creators in upgradeRoom (MSC4289) ([#5173](https://github.com/matrix-org/matrix-js-sdk/pull/5173)). Contributed by @andybalaam.
* [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId ([#5166](https://github.com/matrix-org/matrix-js-sdk/pull/5166)). Contributed by @toger5.
* [MatrixRTC] Do not send the `livekit_alias` in sticky events ([#5165](https://github.com/matrix-org/matrix-js-sdk/pull/5165)). Contributed by @toger5.
* Improve startup performance by using `promise.all` when processing rooms from sync ([#5095](https://github.com/matrix-org/matrix-js-sdk/pull/5095)). Contributed by @MidhunSureshR.
* Add OAuthGrantType enum for OAuth 2.0 API grant types ([#5161](https://github.com/matrix-org/matrix-js-sdk/pull/5161)). Contributed by @hughns.
* Add support for stable OAuth2.0 aware feature from MSC3824 ([#5159](https://github.com/matrix-org/matrix-js-sdk/pull/5159)). Contributed by @hughns.
* Give RoomWidgetClient the ability to send and receive sticky events ([#5142](https://github.com/matrix-org/matrix-js-sdk/pull/5142)). Contributed by @robintown.
## 🐛 Bug Fixes
* [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated ([#5168](https://github.com/matrix-org/matrix-js-sdk/pull/5168)). Contributed by @toger5.
* [MatrixRTC] Fix delayId not resetting on leave ([#5156](https://github.com/matrix-org/matrix-js-sdk/pull/5156)). Contributed by @toger5.
Changes in [40.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.1.0) (2026-01-27)
==================================================================================================
## 🦖 Deprecations
* Deprecate unused `EventShieldReason` reason codes ([#5127](https://github.com/matrix-org/matrix-js-sdk/pull/5127)). Contributed by @richvdh.
## ✨ Features
* Add stable m.oauth UIA stage enum ([#5138](https://github.com/matrix-org/matrix-js-sdk/pull/5138)). Contributed by @hughns.
* Add `MatrixEvent.getKeyForwardingUser` ([#5128](https://github.com/matrix-org/matrix-js-sdk/pull/5128)). Contributed by @richvdh.
* Add types for (unstable) policy servers ([#5116](https://github.com/matrix-org/matrix-js-sdk/pull/5116)). Contributed by @turt2live.
## 🐛 Bug Fixes
* [Backport staging] Recalculate room name on loading members ([#5164](https://github.com/matrix-org/matrix-js-sdk/pull/5164)). Contributed by @RiotRobot.
* Avoid rapidly retrying failed requests ([#5146](https://github.com/matrix-org/matrix-js-sdk/pull/5146)). Contributed by @andybalaam.
* [matrixRTC] MatrixRTCSessions, add missing event reemission. ([#5144](https://github.com/matrix-org/matrix-js-sdk/pull/5144)). Contributed by @toger5.
* Use normal base64 encoding for RTC backend identities ([#5129](https://github.com/matrix-org/matrix-js-sdk/pull/5129)). Contributed by @robintown.
* export parseCallNotificationContent and isMyMembership from RTC types ([#5132](https://github.com/matrix-org/matrix-js-sdk/pull/5132)). Contributed by @Half-Shot.
Changes in [40.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.0.0) (2026-01-13)
==================================================================================================
## 🚨 BREAKING CHANGES
* MatrixRTC Pseudonymous livekit identities ([#5110](https://github.com/matrix-org/matrix-js-sdk/pull/5110)). Contributed by @toger5.
## 🦖 Deprecations
* Mark `forwardingCurve25519KeyChain` as deprecated ([#5111](https://github.com/matrix-org/matrix-js-sdk/pull/5111)). Contributed by @richvdh.
* Mark `IEventDecryptionResult` as deprecated ([#5112](https://github.com/matrix-org/matrix-js-sdk/pull/5112)). Contributed by @richvdh.
## ✨ Features
* Implement MSC4387: M\_SAFETY error ([#5107](https://github.com/matrix-org/matrix-js-sdk/pull/5107)). Contributed by @Half-Shot.
* Implement \_unstable\_getRTCTransports for MSC4143 ([#5104](https://github.com/matrix-org/matrix-js-sdk/pull/5104)). Contributed by @Half-Shot.
* Use `membershipID` for session events ([#5105](https://github.com/matrix-org/matrix-js-sdk/pull/5105)). Contributed by @toger5.
## 🐛 Bug Fixes
* Make MatrixRTC encryption key types narrower for TS 5.9 compatibility ([#5117](https://github.com/matrix-org/matrix-js-sdk/pull/5117)). Contributed by @robintown.
* Re-check outgoing requests after processing them ([#5109](https://github.com/matrix-org/matrix-js-sdk/pull/5109)). Contributed by @andybalaam.
* Make token refresher init itself lazily ([#5106](https://github.com/matrix-org/matrix-js-sdk/pull/5106)). Contributed by @dbkr.
Changes in [39.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.4.0) (2025-12-16)
==================================================================================================
## ✨ Features
* Import room key bundles received after invite. ([#5080](https://github.com/matrix-org/matrix-js-sdk/pull/5080)). Contributed by @kaylendog.
## 🐛 Bug Fixes
* Allow msc4354\_sticky\_key to be optional on sticky events. ([#5073](https://github.com/matrix-org/matrix-js-sdk/pull/5073)). Contributed by @Half-Shot.
* Handle all response fields from /context API being optional ([#5089](https://github.com/matrix-org/matrix-js-sdk/pull/5089)). Contributed by @t3chguy.
Changes in [39.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.3.0) (2025-12-02)
==================================================================================================
## 🐛 Bug Fixes
+1 -1
View File
@@ -117,7 +117,7 @@ checks, so please check back after a few minutes.
Your PR should include tests.
For new user facing features in `matrix-js-sdk`, you
must include comprehensive unit tests written in Jest.
must include comprehensive unit tests written in Vitest.
The existing tests can be found under `spec/unit`
It's good practice to write tests alongside the code as it ensures the code is testable from
+9 -9
View File
@@ -41,10 +41,10 @@ endpoints from before Matrix 1.1, for example.
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
guide](https://pnpm.io/installation#using-corepack) if you do not have it already.
`yarn add matrix-js-sdk`
`pnpm add matrix-js-sdk`
```javascript
import * as sdk from "matrix-js-sdk";
@@ -310,7 +310,7 @@ This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. Yo
host the API reference from the source files like this:
```
$ yarn gendoc
$ pnpm gendoc
$ cd docs
$ python -m http.server 8005
```
@@ -453,7 +453,7 @@ want to use this SDK, skip this section._
First, you need to pull in the right build tools:
```
$ yarn install
$ pnpm install
```
## Building
@@ -461,17 +461,17 @@ First, you need to pull in the right build tools:
To build a browser version from scratch when developing:
```
$ yarn build
$ pnpm build
```
To run tests (Jest):
To run tests:
```
$ yarn test
$ pnpm test
```
To run linting:
```
$ yarn lint
$ pnpm lint
```
+2 -10
View File
@@ -7,21 +7,13 @@ module.exports = {
targets: {
esmodules: true,
},
// We want to output ES modules for the final build (mostly to ensure that
// async imports work correctly). However, jest doesn't support ES modules very
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
// when testing.
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
modules: false,
},
],
[
"@babel/preset-typescript",
{
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import`
// specifiers. However, Jest uses the TS source (via babel) and fails to resolve the `.js` names.
// To resolve this,we use the `.ts` names in the source, and rewrite the `import` specifiers to use
// `.js` during transpilation, *except* when we are targetting Jest.
rewriteImportExtensions: process.env.NODE_ENV !== "test",
rewriteImportExtensions: true,
},
],
],
+4 -5
View File
@@ -71,7 +71,7 @@ Unless otherwise specified, the following applies to all code:
11. If a variable is not receiving a value on declaration, its type must be defined.
```typescript
let errorMessage: Optional<string>;
let errorMessage: string;
```
12. Objects can use shorthand declarations, including mixing of types.
@@ -150,8 +150,7 @@ Unless otherwise specified, the following applies to all code:
1. When using `any`, a comment explaining why must be present.
27. `import` should be used instead of `require`, as `require` does not have types.
28. Export only what can be reused.
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
of truly optional parameters.
29. Prefer a type like `X | null` instead of truly optional parameters.
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
takes an argument that is more often not required than required. An example where the
`?` operator is inappropriate is when taking a room ID: typically the caller should
@@ -161,7 +160,7 @@ Unless otherwise specified, the following applies to all code:
```typescript
function doThingWithRoom(
thing: string,
room: Optional<string>, // require the caller to specify
room: string | null, // require the caller to specify
) {
// ...
}
@@ -214,7 +213,7 @@ Unless otherwise specified, the following applies to all code:
## Tests
1. Tests must be written in TypeScript.
2. Jest mocks are declared below imports, but above everything else.
2. Mocks are declared below imports, but above everything else.
3. Use the following convention template:
```typescript
+1 -1
View File
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
yarn lint
pnpm lint
-48
View File
@@ -1,48 +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 type { Config } from "jest";
import { env } from "process";
const config: Config = {
testEnvironment: "node",
testMatch: ["<rootDir>/spec/**/*.spec.{js,ts}"],
setupFilesAfterEnv: ["<rootDir>/spec/setupTests.ts"],
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts}"],
coverageReporters: ["text-summary", "lcov"],
testResultsProcessor: "@casualbot/jest-sonar-reporter",
transformIgnorePatterns: ["/node_modules/(?!(uuid|p-retry|is-network-error)).+$"],
// 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 }],
// 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") {
reporters.push("<rootDir>/spec/slowReporter.cjs");
}
config.reporters = reporters;
}
export default config;
+3 -6
View File
@@ -1,5 +1,8 @@
import { KnipConfig } from "knip";
// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter'
process.env.GITHUB_ACTIONS = "1";
export default {
entry: [
"src/index.ts",
@@ -28,12 +31,6 @@ export default {
"husky",
// Used in script which only runs in environment with `@octokit/rest` installed
"@octokit/rest",
// Used by jest
"jest-environment-jsdom",
"babel-jest",
"ts-node",
// Used by `@babel/plugin-transform-runtime`
"@babel/runtime",
],
ignoreBinaries: [
// Used when available by reusable workflow `.github/workflows/release-make.yml`
+45 -39
View File
@@ -1,27 +1,26 @@
{
"name": "matrix-js-sdk",
"version": "39.3.0",
"version": "41.3.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=22.0.0"
},
"scripts": {
"prepare": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"clean": "rimraf lib",
"build": "yarn clean && yarn build:compile && yarn build:types",
"prepare": "pnpm build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel --delete-dir-on-start src -w -s -d lib --verbose --extensions \".ts,.js\"",
"build": "pnpm build:compile && pnpm build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile": "babel --delete-dir-on-start -d lib --verbose --extensions \".ts,.js\" src",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
"lint": "pnpm lint:types && pnpm lint:js && pnpm lint:workflows",
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
"test": "vitest run",
"test:watch": "vitest watch",
"coverage": "pnpm test --coverage"
},
"repository": {
"type": "git",
@@ -49,19 +48,18 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^15.3.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
"content-type": "^1.0.4",
"jwt-decode": "^4.0.0",
"loglevel": "^1.9.2",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.14.0",
"matrix-widget-api": "^1.16.1",
"oidc-client-ts": "^3.0.1",
"p-retry": "7",
"p-retry": "8",
"sdp-transform": "^3.0.0",
"unhomoglyph": "^1.0.6",
"uuid": "13"
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
"@action-validator/cli": "^0.6.0",
@@ -78,18 +76,19 @@
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@fetch-mock/vitest": "^0.2.18",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
"@stylistic/eslint-plugin": "^5.0.0",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
"@types/jest": "^30.0.0",
"@types/node": "18",
"@types/node": "22",
"@types/sdp-transform": "^2.4.5",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"babel-jest": "^30.0.0",
"@vitest/coverage-v8": "^4.0.17",
"@vitest/eslint-plugin": "^1.6.6",
"@vitest/ui": "^4.0.17",
"babel-plugin-search-and-replace": "^1.1.1",
"debug": "^4.3.4",
"eslint": "8.57.1",
@@ -97,36 +96,43 @@
"eslint-config-prettier": "^10.0.0",
"eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^29.0.0",
"eslint-plugin-jsdoc": "^61.0.0",
"eslint-plugin-jsdoc": "^62.0.0",
"eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-n": "^14.0.0",
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-tsdoc": "^0.5.0",
"eslint-plugin-unicorn": "^56.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "11.1.5",
"fetch-mock-jest": "^1.5.1",
"fetch-mock": "^12.6.0",
"happy-dom": "^20.1.0",
"husky": "^9.0.0",
"jest": "^30.0.0",
"jest-environment-jsdom": "^30.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^30.0.0",
"knip": "^5.0.0",
"knip": "^6.0.0",
"lint-staged": "^16.0.0",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.6.2",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"prettier": "3.8.3",
"typedoc": "^0.28.1",
"typedoc-plugin-coverage": "^4.0.0",
"typedoc-plugin-mdn-links": "^5.0.0",
"typedoc-plugin-missing-exports": "^4.0.0",
"typescript": "^5.4.2"
"typescript": "^6.0.0",
"vitest": "^4.0.17",
"vitest-sonar-reporter": "^3.0.0"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
}
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"eslint": "8"
}
},
"allowedDeprecatedVersions": {
"eslint": "8"
},
"overrides": {
"expect": "30.3.0",
"flatted@<=3.4.1": "^3.4.2",
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
"vite": "8.0.8"
}
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
}
+8353
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
nodeLinker: hoisted
+1 -1
View File
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
sonar.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=spec/**/*
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
sonar.testExecutionReportPaths=coverage/sonar-report.xml
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
+2 -2
View File
@@ -17,7 +17,7 @@ limitations under the License.
*/
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
/* eslint-disable jest/no-standalone-expect */
/* eslint-disable @vitest/no-standalone-expect */
import MockHttpBackend from "matrix-mock-request";
@@ -39,7 +39,7 @@ import { type ISyncResponder } from "./test-utils/SyncResponder";
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
* Instead, construct a MatrixClient manually, use fetch-mock to intercept the HTTP requests, and
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
*/
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
+29 -47
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import debug from "debug";
@@ -74,7 +74,7 @@ describe("cross-signing", () => {
function createCryptoCallbacks(): CryptoCallbacks {
return {
getSecretStorageKey: (keys, name) => {
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
return Promise.resolve<[string, Uint8Array<ArrayBuffer>]>(["key_id", encryptionKey]);
},
};
}
@@ -83,7 +83,6 @@ describe("cross-signing", () => {
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({
@@ -113,8 +112,7 @@ describe("cross-signing", () => {
);
afterEach(async () => {
await aliceClient.stopClient();
fetchMock.mockReset();
aliceClient.stopClient();
});
/**
@@ -138,27 +136,25 @@ describe("cross-signing", () => {
await bootstrapCrossSigning(authDict);
// check that the cross-signing keys have been uploaded
expect(fetchMock.called("upload-cross-signing-keys")).toBeTruthy();
const [, keysOpts] = fetchMock.lastCall("upload-cross-signing-keys")!;
expect(fetchMock.callHistory.called("upload-cross-signing-keys")).toBeTruthy();
const keysOpts = fetchMock.callHistory.lastCall("upload-cross-signing-keys")!.options;
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}]`);
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}]`);
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")!;
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
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}]`,
);
expect(body).toHaveProperty([TEST_USER_ID, TEST_DEVICE_ID, "signatures", TEST_USER_ID, sskId]);
});
it("get cross signing keys from secret storage and import them", async () => {
@@ -237,13 +233,17 @@ describe("cross-signing", () => {
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
// Expect the signature to be uploaded
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
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}]`,
);
expect(body).toHaveProperty([
TEST_USER_ID,
TEST_DEVICE_ID,
"signatures",
TEST_USER_ID,
`ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}`,
]);
});
it("can bootstrapCrossSigning twice", async () => {
@@ -255,8 +255,7 @@ describe("cross-signing", () => {
// a second call should do nothing except GET requests
fetchMock.mockClear();
await bootstrapCrossSigning(authDict);
const calls = fetchMock.calls((url, opts) => opts.method != "GET");
expect(calls.length).toEqual(0);
expect(fetchMock).toHaveFetchedTimes(0, "unmatched");
});
it("will upload existing cross-signing keys to an established secret storage", async () => {
@@ -267,7 +266,6 @@ describe("cross-signing", () => {
// To arrange that, we call `bootstrapCrossSigning` on our main device, and then (pretend to) set up 4S from
// a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
mockSetupCrossSigningRequests();
const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
accountDataAccumulator.interceptGetAccountData();
@@ -282,7 +280,7 @@ describe("cross-signing", () => {
});
// Prepare for the cross-signing keys
const p = accountDataAccumulator.interceptSetAccountData(":type(m.cross_signing..*)");
const p = accountDataAccumulator.waitForAccountData("m.cross_signing.master");
await bootstrapCrossSigning(authDict);
await p;
@@ -403,7 +401,7 @@ describe("cross-signing", () => {
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeFalsy();
});
}, 10000);
});
describe("getCrossSigningKeyId", () => {
@@ -415,22 +413,13 @@ describe("cross-signing", () => {
*/
function awaitCrossSigningKeysUpload() {
return new Promise<any>((resolve) => {
fetchMock.post(
{
url: new URL(
"/_matrix/client/v3/keys/device_signing/upload",
aliceClient.getHomeserverUrl(),
).toString(),
name: "upload-cross-signing-keys",
},
(url, options) => {
const content = JSON.parse(options.body as string);
fetchMock.modifyRoute("upload-cross-signing-keys", {
response: (callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return {};
},
// Override the route defined in E2EKeyReceiver
{ overwriteRoutes: true },
);
});
});
}
@@ -461,9 +450,6 @@ describe("cross-signing", () => {
describe("crossSignDevice", () => {
beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
// make sure that there is another device which we can sign
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
@@ -477,10 +463,6 @@ describe("cross-signing", () => {
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
});
afterEach(async () => {
jest.useRealTimers();
});
it("fails for an unknown device", async () => {
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
});
@@ -493,9 +475,9 @@ describe("cross-signing", () => {
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
// check that a sig for the device was uploaded
const calls = fetchMock.calls("upload-sigs");
const calls = fetchMock.callHistory.calls("upload-sigs");
expect(calls.length).toEqual(1);
const body = JSON.parse(calls[0][1]!.body as string);
const body = JSON.parse(calls[0].options!.body as string);
const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
expect(deviceSig).toHaveProperty("signatures");
});
+203 -129
View File
@@ -16,12 +16,12 @@ limitations under the License.
*/
import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import Olm from "@matrix-org/olm";
import { type RouteResponse } from "fetch-mock";
import type FetchMock from "fetch-mock";
import * as testUtils from "../../test-utils/test-utils";
import {
emitPromise,
@@ -86,6 +86,7 @@ import {
encryptGroupSessionKey,
encryptMegolmEvent,
encryptMegolmEventRawPlainText,
encryptOlmEvent,
establishOlmSession,
getTestOlmAccountKeys,
expectSendRoomKey,
@@ -104,7 +105,7 @@ afterEach(() => {
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
jest.useRealTimers();
vi.useRealTimers();
});
describe("crypto", () => {
@@ -154,13 +155,8 @@ describe("crypto", () => {
return response;
}
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
fetchMock.postOnce(
new RegExp(rootRegexp + "(r0|v3)/keys/query"),
(url: string, opts: RequestInit) => onQueryRequest(JSON.parse(opts.body as string)),
{
// append to the list of intercepts on this path
overwriteRoutes: false,
},
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/query"), (callLog) =>
onQueryRequest(JSON.parse(callLog.options.body as string)),
);
}
@@ -170,7 +166,7 @@ describe("crypto", () => {
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
* (or a function that returns one).
*/
function expectAliceKeyClaim(response: FetchMock.MockResponse | FetchMock.MockResponseFunction) {
function expectAliceKeyClaim(response: RouteResponse) {
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
}
@@ -223,15 +219,20 @@ describe("crypto", () => {
*/
function createCryptoCallbacks(): CryptoCallbacks {
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
let cachedKey: { keyId: string; key: Uint8Array };
const cacheSecretStorageKey = (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => {
let cachedKey: { keyId: string; key: Uint8Array<ArrayBuffer> };
const cacheSecretStorageKey = (
keyId: string,
keyInfo: SecretStorageKeyDescription,
key: Uint8Array<ArrayBuffer>,
) => {
cachedKey = {
keyId,
key,
};
};
const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);
const getSecretStorageKey = () =>
Promise.resolve<[string, Uint8Array<ArrayBuffer>]>([cachedKey.keyId, cachedKey.key]);
return {
cacheSecretStorageKey,
@@ -243,7 +244,6 @@ describe("crypto", () => {
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({
@@ -265,18 +265,20 @@ describe("crypto", () => {
testOlmAccount = await createOlmAccount();
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
vi.useRealTimers();
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
);
afterEach(async () => {
await aliceClient.stopClient();
aliceClient.stopClient();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
if (vi.isFakeTimers()) {
await vi.runAllTimersAsync();
}
});
it("MatrixClient.getCrypto returns a CryptoApi", () => {
@@ -343,7 +345,7 @@ describe("crypto", () => {
describe("Unable to decrypt error codes", function () {
beforeEach(() => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
});
it("Decryption fails with UISI error", async () => {
@@ -719,7 +721,7 @@ describe("crypto", () => {
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
await awaitDecryptionError;
await expect(awaitDecryptionError).resolves.toBeUndefined();
});
});
@@ -870,6 +872,7 @@ describe("crypto", () => {
await expectSendRoomKey("@bob:xyz", testOlmAccount);
});
// eslint-disable-next-line @vitest/expect-expect
it("Alice sends a megolm message", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -897,6 +900,7 @@ describe("crypto", () => {
]);
});
// eslint-disable-next-line @vitest/expect-expect
it("We should start a new megolm session after forceDiscardSession", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -997,9 +1001,7 @@ describe("crypto", () => {
await startClientAndAwaitFirstSync();
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount);
// We need to fake the timers to advance the time, but the wasm bindings of matrix-sdk-crypto rely on a
// working `queueMicrotask`
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
const syncResponse = getSyncResponse(["@bob:xyz"]);
@@ -1031,7 +1033,7 @@ describe("crypto", () => {
expect(sessionId).toBeDefined();
// Advance the time by 1h
jest.advanceTimersByTime(oneHourInMs);
vi.advanceTimersByTime(oneHourInMs);
// Send a second message to bob and get the encrypted message
const [secondEncryptedMessage] = await Promise.all([
@@ -1152,7 +1154,7 @@ describe("crypto", () => {
// it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID);
expect(decryptedEvent.getContent()).toEqual({});
expect(decryptedEvent.getContent<IContent>()).toEqual({});
expect(decryptedEvent.getClearContent()).toBeUndefined();
});
@@ -1176,20 +1178,12 @@ describe("crypto", () => {
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
// ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes.
const sendRoomMessageResolvers = Promise.withResolvers<FetchMock.MockResponse>();
const sendRoomMessageResolvers = Promise.withResolvers<RouteResponse>();
const reqProm = new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp("/send/m.room.encrypted/"),
async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => {
resolve(JSON.parse(opts.body as string));
return await sendRoomMessageResolvers.promise;
},
{
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
overwriteRoutes: false,
},
);
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), async (callLog): Promise<RouteResponse> => {
resolve(JSON.parse(callLog.options.body as string));
return await sendRoomMessageResolvers.promise;
});
});
// Now we start to send the message
@@ -1272,6 +1266,7 @@ describe("crypto", () => {
});
}
// eslint-disable-next-line @vitest/expect-expect
it("Sending an event initiates a member list sync", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -1295,6 +1290,7 @@ describe("crypto", () => {
await Promise.all([sendPromise, megolmMessagePromise, memberListPromise]);
});
// eslint-disable-next-line @vitest/expect-expect
it("loading the membership list inhibits a later load", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -1404,33 +1400,26 @@ describe("crypto", () => {
describe("key upload request", () => {
beforeEach(() => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
});
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
return new Promise((resolve) => {
const listener = (url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
const keysCount = Object.keys(content?.one_time_keys || {}).length;
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
if (keysCount) resolve({ keysCount, fallbackKeysCount });
return {
one_time_key_counts: {
// The matrix client does `/upload` requests until 50 keys are uploaded
// We return here 60 to avoid the `/upload` request loop
signed_curve25519: keysCount ? 60 : keysCount,
},
};
};
for (const path of ["/_matrix/client/v3/keys/upload", "/_matrix/client/v3/keys/upload"]) {
fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener, {
// These routes are already defined in the E2EKeyReceiver
// We want to overwrite the behaviour of the E2EKeyReceiver
overwriteRoutes: true,
});
}
fetchMock.modifyRoute("keys-upload", {
response: (callLog) => {
const content = JSON.parse(callLog.options.body as string);
const keysCount = Object.keys(content?.one_time_keys || {}).length;
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
if (keysCount) resolve({ keysCount, fallbackKeysCount });
return {
one_time_key_counts: {
// The matrix client does `/upload` requests until 50 keys are uploaded
// We return here 60 to avoid the `/upload` request loop
signed_curve25519: keysCount ? 60 : keysCount,
},
};
},
});
});
}
@@ -1443,7 +1432,7 @@ describe("crypto", () => {
await syncPromise(aliceClient);
// Verify that `/upload` is called on Alice's homesever
// Verify that `/upload` is called on Alice's homeserver
const { keysCount, fallbackKeysCount } = await uploadPromise;
expect(keysCount).toBeGreaterThan(0);
expect(fallbackKeysCount).toBe(0);
@@ -1457,7 +1446,7 @@ describe("crypto", () => {
// Advance local date to 2 minutes
// The old crypto only runs the upload every 60 seconds
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
vi.setSystemTime(Date.now() + 2 * 60 * 1000);
await syncPromise(aliceClient);
@@ -1548,18 +1537,16 @@ describe("crypto", () => {
function awaitKeyQueryRequest(): Promise<Record<string, []>> {
return new Promise((resolve) => {
const listener = (url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
// Resolve with request payload
resolve(content.device_keys);
// Return response of `/keys/query`
return queryResponseBody;
};
fetchMock.post(
new URL("/_matrix/client/v3/keys/query", aliceClient.getHomeserverUrl()).toString(),
listener,
(callLog) => {
const content = JSON.parse(callLog.options.body as string);
// Resolve with request payload
resolve(content.device_keys);
// Return response of `/keys/query`
return queryResponseBody;
},
);
});
}
@@ -1598,8 +1585,7 @@ describe("crypto", () => {
});
it("Get devices from tracked users", async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -1611,12 +1597,12 @@ describe("crypto", () => {
// Advance local date to 2 minutes
// The old crypto only runs the upload every 60 seconds
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
vi.setSystemTime(Date.now() + 2 * 60 * 1000);
await syncPromise(aliceClient);
// Old crypto: for alice: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
jest.runAllTimers();
vi.runAllTimers();
// Old crypto: for alice: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
await flushPromises();
@@ -1624,7 +1610,7 @@ describe("crypto", () => {
await queryPromise;
// Old crypto: for `user`: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
jest.runAllTimers();
vi.runAllTimers();
// Old crypto: for `user`: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
// It will add `@testing_florian1:matrix.org` devices to the DeviceList
await flushPromises();
@@ -1648,7 +1634,7 @@ describe("crypto", () => {
* Create a fake secret storage key
* Async because `bootstrapSecretStorage` expect an async method
*/
const createSecretStorageKey = jest.fn().mockResolvedValue({
const createSecretStorageKey = vi.fn().mockResolvedValue({
keyInfo: {}, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
});
@@ -1666,7 +1652,7 @@ describe("crypto", () => {
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
*/
async function awaitCrossSigningKeyUpload(key: string): Promise<Record<string, {}>> {
const content = await accountDataAccumulator.interceptSetAccountData(`m.cross_signing.${key}`);
const content = await accountDataAccumulator.waitForAccountData(`m.cross_signing.${key}`);
return content.encrypted;
}
@@ -1678,10 +1664,7 @@ describe("crypto", () => {
async function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
// eslint-disable-next-line no-constant-condition
while (true) {
const content = await accountDataAccumulator.interceptSetAccountData(":type(m.secret_storage.*)", {
repeat: 1,
overwriteRoutes: true,
});
const content = await accountDataAccumulator.waitForAccountData("m.secret_storage.*");
if (content.key) {
return content.key;
}
@@ -1689,10 +1672,7 @@ describe("crypto", () => {
}
async function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
const content = await accountDataAccumulator.interceptSetAccountData("m.megolm_backup.v1", {
repeat: 1,
overwriteRoutes: true,
});
const content = await accountDataAccumulator.waitForAccountData("m.megolm_backup.v1");
return content.encrypted;
}
@@ -1713,7 +1693,6 @@ describe("crypto", () => {
* @param backupVersion - The version of the created backup
*/
async function bootstrapSecurity(backupVersion: string): Promise<void> {
mockSetupCrossSigningRequests();
mockSetupMegolmBackupRequests(backupVersion);
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
@@ -1792,6 +1771,7 @@ describe("crypto", () => {
it("Should create a 4S key", async () => {
accountDataAccumulator.interceptGetAccountData();
accountDataAccumulator.interceptSetAccountData();
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
@@ -1943,8 +1923,7 @@ describe("crypto", () => {
describe("Manage Key Backup", () => {
beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
});
it("Should be able to restore from 4S after bootstrap", async () => {
@@ -1961,29 +1940,23 @@ describe("crypto", () => {
const newKey = testData.MEGOLM_SESSION_DATA;
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
resolve(uploadPayload);
return {
status: 200,
body: {
count: 1,
etag: "abcdefg",
},
};
},
{
overwriteRoutes: true,
},
);
fetchMock.put("path:/_matrix/client/v3/room_keys/keys", (callLog) => {
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
resolve(uploadPayload);
return {
status: 200,
body: {
count: 1,
etag: "abcdefg",
},
};
});
});
await aliceClient.getCrypto()!.importRoomKeys([newKey]);
// The backup loop waits a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
vi.runAllTimers();
const keyBackupData = await awaitKeyUploaded;
@@ -2011,40 +1984,33 @@ describe("crypto", () => {
fetchMock.delete(
"express:/_matrix/client/v3/room_keys/version/:version",
(url: string, options: RequestInit) => {
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
fetchMock.modifyRoute("room-keys-version", {
response: {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
},
{ overwriteRoutes: true },
);
});
resolve();
return {};
},
{ overwriteRoutes: true },
);
});
const newVersion = "2";
fetchMock.post(
"path:/_matrix/client/v3/room_keys/version",
(url, request) => {
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
fetchMock.modifyRoute("post-room-keys-version", {
response: (callLog) => {
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
backupData.version = newVersion;
backupData.count = 0;
backupData.etag = "zer";
// update get call with new version
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
overwriteRoutes: true,
});
fetchMock.modifyRoute("room-keys-version", { response: backupData });
return {
version: backupVersion,
};
},
{ overwriteRoutes: true },
);
});
const newBackupStatusUpdate = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
@@ -2099,6 +2065,7 @@ describe("crypto", () => {
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.known).toBe(false); // We haven't actually stashed a copy of Alice's identity
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2106,15 +2073,15 @@ describe("crypto", () => {
});
it("Cross signing keys are available for a tracked user", async () => {
// Process Alice keys, old crypto has a sleep(5ms) during the process
await jest.advanceTimersByTimeAsync(5);
// Process Alice keys
await flushPromises();
// Alice is the local user and should be tracked !
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
expect(verificationStatus.known).toBe(true);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2125,7 +2092,8 @@ describe("crypto", () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
expect(hasCrossSigningKeysForUser).toBe(false);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus("@unknown:xyz");
expect(verificationStatus.known).toBe(false);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2155,6 +2123,7 @@ describe("crypto", () => {
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.known).toBe(true);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2195,6 +2164,7 @@ describe("crypto", () => {
client2?.stopClient();
});
// eslint-disable-next-line @vitest/expect-expect
test("Sending a message in a room where the server is hiding the state event does not send a plaintext event", async () => {
// Alice is in an encrypted room
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
@@ -2252,6 +2222,7 @@ describe("crypto", () => {
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
});
// eslint-disable-next-line @vitest/expect-expect
test("Changes to the rotation period should be ignored after a client restart", async () => {
// Alice is in an encrypted room, where the rotation period is set to 2 messages
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
@@ -2345,4 +2316,107 @@ describe("crypto", () => {
);
}
});
describe("secret pushing", () => {
it("should push a new backup key when a new backup key is set", async () => {
// setup: alice has another device, DEVICE_ID, which is verified
const crypto = aliceClient.getCrypto()!;
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
await startClientAndAwaitFirstSync();
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
// when we set a new backup key
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", {
status: 200,
body: { version: "1" },
});
const secretPushPromise = new Promise<any>((resolve) => {
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return {};
});
});
await crypto.resetKeyBackup();
// we expect the other device to get a secret push
const content = await secretPushPromise;
const curve25519key = JSON.parse(testOlmAccount.identity_keys()).curve25519;
const ciphertext = content.messages["@alice:localhost"].DEVICE_ID.ciphertext[curve25519key];
const olmSession = new Olm.Session();
olmSession.create_inbound(testOlmAccount, ciphertext.body);
const decrypted = JSON.parse(olmSession.decrypt(0, ciphertext.body));
expect(decrypted.type).toBe("io.element.msc4385.secret.push");
expect(decrypted.content.name).toBe("m.megolm_backup.v1");
});
it("should receive pushed backup key", async () => {
// setup: alice has another device, DEVICE_ID, which is verified,
// and has a key backup set up and signed by DEVICE_ID
const crypto = aliceClient.getCrypto()!;
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
await startClientAndAwaitFirstSync();
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
// after we push the backup key to alice...
const senderIdentityKeys = JSON.parse(testOlmAccount.identity_keys());
const aliceDeviceKeys = await crypto.getOwnDeviceKeys();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const secretPush = encryptOlmEvent({
sender: "@alice:localhost",
senderKey: senderIdentityKeys.curve25519,
senderSigningKey: senderIdentityKeys.ed25519,
p2pSession,
recipient: "@alice:localhost",
recipientCurve25519Key: aliceDeviceKeys.curve25519,
recipientEd25519Key: aliceDeviceKeys.ed25519,
plaincontent: {
secret: testData.BACKUP_DECRYPTION_KEY_BASE64,
name: "m.megolm_backup.v1",
},
plaintype: "io.element.msc4385.secret.push",
});
const syncResponse = {
next_batch: 1,
to_device: {
events: [secretPush],
},
};
const backupKeyReceivedPromise = new Promise<string>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupDecryptionKeyCached, resolve);
});
const keyBackupEnabledPromise = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
// alice should be using backup now
expect(await backupKeyReceivedPromise).toBe(testData.SIGNED_BACKUP_DATA.version);
await keyBackupEnabledPromise;
expect(await crypto.getActiveSessionBackupVersion()).toBe(testData.SIGNED_BACKUP_DATA.version);
});
});
});
+54 -42
View File
@@ -15,7 +15,8 @@ limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type CallLog } from "fetch-mock";
import debug from "debug";
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
@@ -28,7 +29,7 @@ import { emitPromise, EventCounter } from "../../test-utils/test-utils";
describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -59,28 +60,35 @@ describe("Device dehydration", () => {
});
const crypto = matrixClient.getCrypto()!;
fetchMock.config.overwriteRoutes = true;
// start dehydration -- we start with no dehydrated device, and we
// store the dehydrated device that we create
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
fetchMock.get(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
},
});
{ name: "get-dehydrated-device" },
);
let dehydratedDeviceBody: any;
let dehydrationCount = 0;
let resolveDehydrationPromise: () => void;
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
dehydratedDeviceBody = JSON.parse(opts.body as string);
dehydrationCount++;
if (resolveDehydrationPromise) {
resolveDehydrationPromise();
}
return {};
});
fetchMock.put(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
(callLog) => {
dehydratedDeviceBody = JSON.parse(callLog.options.body as string);
dehydrationCount++;
if (resolveDehydrationPromise) {
resolveDehydrationPromise();
}
return {};
},
{ name: "put-dehydrated-device" },
);
await crypto.startDehydration();
expect(dehydrationCount).toEqual(1);
@@ -91,7 +99,7 @@ describe("Device dehydration", () => {
const dehydrationPromise = new Promise<void>((resolve, reject) => {
resolveDehydrationPromise = resolve;
});
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise;
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
@@ -101,16 +109,18 @@ describe("Device dehydration", () => {
// restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so
// a new dehydration key will be set
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
fetchMock.modifyRoute("get-dehydrated-device", {
response: {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
},
});
const eventsResponse = jest.fn((url, opts) => {
const eventsResponse = vi.fn((callLog: CallLog) => {
// rehydrating should make two calls to the /events endpoint.
// The first time will return a single event, and the second
// time will return no events (which will signal to the
// rehydration function that it can stop)
const body = JSON.parse(opts.body as string);
const body = JSON.parse(callLog.options.body as string);
const nextBatch = body.next_batch ?? "0";
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
return {
@@ -135,28 +145,30 @@ describe("Device dehydration", () => {
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
// test that if we get an error when we try to rotate, it emits an event
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
fetchMock.modifyRoute("put-dehydrated-device", {
response: {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
},
});
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await rotationErrorEventPromise;
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
fetchMock.modifyRoute("get-dehydrated-device", {
response: {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
},
});
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
return {};
});
fetchMock.modifyRoute("put-dehydrated-device", { response: { body: {} } });
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
await crypto.startDehydration(true);
await rehydrationErrorEventPromise;
@@ -182,8 +194,8 @@ async function initializeSecretStorage(
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
const accountData: Map<string, object> = new Map();
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
const name = callLog.url.split("/").pop()!;
const value = accountData.get(name);
if (value) {
return value;
@@ -197,9 +209,9 @@ async function initializeSecretStorage(
};
}
});
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
const value = JSON.parse(opts.body as string);
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
const name = callLog.url.split("/").pop()!;
const value = JSON.parse(callLog.options.body as string);
accountData.set(name, value);
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
return {};
File diff suppressed because it is too large Load Diff
+131 -139
View File
@@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { type Mocked } from "jest-mock";
import { type Mocked } from "vitest";
import {
createClient,
encodeBase64,
type IContent,
type ICreateClientOpts,
type IEvent,
type IMegolmSessionData,
@@ -36,7 +37,13 @@ import { advanceTimersUntil, awaitDecryption, syncPromise } from "../../test-uti
import * as testData from "../../test-utils/test-data";
import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { flushPromises } from "../../test-utils/flushPromises";
import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent, type CryptoApi } from "../../../src/crypto-api";
import {
decodeRecoveryKey,
DecryptionFailureCode,
CryptoEvent,
type CryptoApi,
DecryptionKeyDoesNotMatchError,
} from "../../../src/crypto-api";
import { type KeyBackup } from "../../../src/rust-crypto/backup.ts";
const ROOM_ID = testData.TEST_ROOM_ID;
@@ -69,10 +76,11 @@ function mockUploadEmitter(
expectedVersion: string,
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
const emitter = new TypedEventEmitter();
fetchMock.removeRoute("mock-upload-emitter");
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const version = new URLSearchParams(new URL(url).search).get("version");
(callLog) => {
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version != expectedVersion) {
return {
status: 403,
@@ -83,7 +91,7 @@ function mockUploadEmitter(
},
};
}
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) {
@@ -99,9 +107,7 @@ function mockUploadEmitter(
},
};
},
{
overwriteRoutes: true,
},
{ name: "mock-upload-emitter" },
);
return emitter;
}
@@ -117,12 +123,10 @@ describe("megolm-keys backup", () => {
let e2eKeyResponder: E2EKeyResponder;
beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
// 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);
@@ -133,15 +137,12 @@ describe("megolm-keys backup", () => {
});
afterEach(async () => {
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}
aliceClient?.stopClient();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
await vi.runAllTimersAsync();
fetchMock.mockReset();
jest.restoreAllMocks();
vi.restoreAllMocks();
});
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
@@ -207,9 +208,9 @@ describe("megolm-keys backup", () => {
);
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (callLog) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA;
} else {
@@ -237,11 +238,11 @@ describe("megolm-keys backup", () => {
// Eventually, decryption succeeds.
await awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
});
it("handles error on backup query gracefully", async () => {
jest.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
@@ -253,9 +254,9 @@ describe("megolm-keys backup", () => {
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest();
const calls = fetchMock.calls("getKey");
const calls = fetchMock.callHistory.calls("getKey");
expect(calls.length).toEqual(1);
expect(calls[0][0]).toEqual(EXPECTED_URL);
expect(calls[0].url).toEqual(EXPECTED_URL);
await flushBackupRequest();
@@ -274,11 +275,11 @@ describe("megolm-keys backup", () => {
// Send Alice a message that she won't be able to decrypt
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest();
const calls = fetchMock.calls("getKey");
const calls = fetchMock.callHistory.calls("getKey");
expect(calls.length).toEqual(1);
expect(calls[0][0]).toEqual(EXPECTED_URL);
expect(calls[0].url).toEqual(EXPECTED_URL);
fetchMock.resetHistory();
fetchMock.clearHistory();
// another message
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
@@ -288,7 +289,7 @@ describe("megolm-keys backup", () => {
};
syncResponder.sendOrQueueSyncResponse(syncResponse2);
await flushBackupRequest();
expect(fetchMock.calls("getKey").length).toEqual(0);
expect(fetchMock.callHistory.calls("getKey").length).toEqual(0);
});
});
@@ -364,9 +365,9 @@ describe("megolm-keys backup", () => {
}
it("Should import full backup in chunks", async function () {
const importMockImpl = jest.fn();
const importMockImpl = vi.fn();
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
// We need several rooms with several sessions to test chunking
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
@@ -380,7 +381,7 @@ describe("megolm-keys backup", () => {
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const progressCallback = vi.fn();
const result = await aliceCrypto.restoreKeyBackup({
progressCallback,
});
@@ -417,7 +418,7 @@ describe("megolm-keys backup", () => {
});
it("Should continue to process backup if a chunk import fails and report failures", async function () {
const importMockImpl = jest
const importMockImpl = vi
.fn()
.mockImplementationOnce(() => {
// Fail to import first chunk
@@ -427,7 +428,7 @@ describe("megolm-keys backup", () => {
.mockResolvedValue(undefined);
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
@@ -439,7 +440,7 @@ describe("megolm-keys backup", () => {
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const progressCallback = vi.fn();
const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
expect(result.total).toStrictEqual(expectedTotal);
@@ -463,13 +464,13 @@ describe("megolm-keys backup", () => {
it("Should continue if some keys fails to decrypt", async function () {
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = jest.fn();
aliceCrypto.importBackedUpRoomKeys = vi.fn();
const decryptionFailureCount = 2;
const mockDecryptor = {
// DecryptSessions does not reject on decryption failure, but just skip the key
decryptSessions: jest.fn().mockImplementation((sessions) => {
decryptSessions: vi.fn().mockImplementation((sessions) => {
// simulate fail to decrypt 2 keys out of all
const decrypted = [];
const keys = Object.keys(sessions);
@@ -480,11 +481,11 @@ describe("megolm-keys backup", () => {
}
return decrypted;
}),
free: jest.fn(),
free: vi.fn(),
};
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.getBackupDecryptor = jest.fn().mockResolvedValue(mockDecryptor);
aliceCrypto.getBackupDecryptor = vi.fn().mockResolvedValue(mockDecryptor);
const { response, expectedTotal } = createBackupDownloadResponse([100]);
@@ -505,17 +506,12 @@ describe("megolm-keys backup", () => {
it("Should get the decryption key from the secret storage and restore the key backup", async function () {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
const fullBackup = {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
const fullBackup = createFullBackup(
testData.MEGOLM_SESSION_DATA.session_id,
testData.CURVE25519_KEY_BACKUP_DATA,
);
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
@@ -526,15 +522,44 @@ describe("megolm-keys backup", () => {
expect(result.imported).toStrictEqual(1);
});
it("Should throw an error if the decryption key does not match the backup", async function () {
// Given the stored backup decryption key does not match the public backup info
// @ts-ignore - mock a private method for testing purpose
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64_ALT);
const fullBackup = createFullBackup(
testData.MEGOLM_SESSION_DATA.session_id,
testData.CURVE25519_KEY_BACKUP_DATA,
);
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
// When we load that key, we throw because the keys don't match
await expect(aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage()).rejects.toThrow(
DecryptionKeyDoesNotMatchError,
);
});
it("Should throw an error if the decryption key is not found in cache", async () => {
await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store");
});
function createFullBackup(sessionId: string, data: KeyBackupSession) {
return {
rooms: {
[ROOM_ID]: {
sessions: {
[sessionId]: data,
},
},
},
};
}
});
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);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404, { name: "room-keys-version" });
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -571,8 +596,8 @@ describe("megolm-keys backup", () => {
});
});
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
fetchMock.modifyRoute("room-keys-version", {
response: { status: 200, body: testData.SIGNED_BACKUP_DATA },
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
@@ -581,7 +606,7 @@ describe("megolm-keys backup", () => {
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();
vi.runAllTimers();
await Promise.all(uploadPromises);
@@ -605,7 +630,7 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
vi.runAllTimers();
await newKeyUploadPromise;
});
@@ -630,7 +655,7 @@ describe("megolm-keys backup", () => {
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
name: "room-keys-version",
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
@@ -640,7 +665,7 @@ describe("megolm-keys backup", () => {
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();
vi.runAllTimers();
// wait for all keys to be backed up
await remainingZeroPromise;
@@ -651,10 +676,7 @@ describe("megolm-keys backup", () => {
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,
});
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
// 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
@@ -697,12 +719,12 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
vi.runAllTimers();
await disableOldBackup;
await enableNewBackup;
jest.runAllTimers();
vi.runAllTimers();
await Promise.all(uploadPromises);
await newKeyUploadPromise;
@@ -717,22 +739,14 @@ describe("megolm-keys 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,
});
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// 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,
},
);
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
resolve(undefined);
throw new TypeError(`Failed to fetch`);
});
});
// kick the import loop off and wait for the failed request
@@ -741,27 +755,21 @@ describe("megolm-keys backup", () => {
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
jest.advanceTimersByTime(10 * 60 * 1000);
vi.advanceTimersByTime(10 * 60 * 1000);
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,
},
);
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
resolve(undefined);
return {
status: 200,
body: {
count: 2,
etag: "abcdefg",
},
};
});
});
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
@@ -774,7 +782,7 @@ describe("megolm-keys backup", () => {
});
// run the timers, which will make the backup loop redo the request
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
await vi.advanceTimersByTimeAsync(10 * 60 * 1000);
await successPromise;
await allKeysUploadedPromise;
});
@@ -782,7 +790,7 @@ describe("megolm-keys backup", () => {
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);
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -801,9 +809,7 @@ describe("megolm-keys backup", () => {
// 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,
});
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", unsignedBackup);
const checked = await aliceCrypto.checkKeyBackupAndEnable();
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
@@ -813,9 +819,7 @@ describe("megolm-keys backup", () => {
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,
});
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// check that signalling is working
const backupPromise = new Promise<void>((resolve, reject) => {
@@ -837,7 +841,7 @@ describe("megolm-keys backup", () => {
it("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
aliceClient = await initTestClient();
@@ -853,11 +857,9 @@ describe("megolm-keys backup", () => {
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
// Return now the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
expect(await aliceCrypto.getKeyBackupInfo()).toMatchObject(testData.SIGNED_BACKUP_DATA);
// Delete the backup and we are expecting the key backup to be disabled
const keyBackupStatus = Promise.withResolvers<boolean>();
@@ -991,7 +993,7 @@ describe("megolm-keys 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);
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
@@ -1001,9 +1003,7 @@ describe("megolm-keys backup", () => {
delete unsignedBackup.auth_data.signatures;
unsignedBackup.version = "2";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", unsignedBackup);
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
@@ -1018,7 +1018,7 @@ describe("megolm-keys 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);
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
@@ -1028,9 +1028,7 @@ describe("megolm-keys backup", () => {
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,
});
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", newBackup);
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
@@ -1045,25 +1043,19 @@ describe("megolm-keys 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);
fetchMock.getOnce("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",
},
fetchMock.getOnce("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();
@@ -1072,10 +1064,12 @@ describe("megolm-keys backup", () => {
describe("Backup Changed from other sessions", () => {
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
name: "room-keys-version",
});
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
fetchMock.getOnce("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -1108,9 +1102,9 @@ describe("megolm-keys backup", () => {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
(url, request) => {
(callLog) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA;
} else {
@@ -1124,7 +1118,7 @@ describe("megolm-keys backup", () => {
};
}
},
{ overwriteRoutes: true },
{ name: "room-keys" },
);
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
@@ -1135,7 +1129,7 @@ describe("megolm-keys backup", () => {
const event = room.getLiveTimeline().getEvents()[0];
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
// =====
// Second suppose now that the backup has changed to version 2
@@ -1146,7 +1140,7 @@ describe("megolm-keys backup", () => {
version: "2",
};
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, { overwriteRoutes: true });
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
// suppose the new key is now known
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
@@ -1159,11 +1153,10 @@ describe("megolm-keys backup", () => {
const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
(url, request) => {
fetchMock.modifyRoute("room-keys", {
response: (callLog) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version == newBackup.version) {
awaitHasQueriedNewBackup.resolve();
return testData.CURVE25519_KEY_BACKUP_DATA;
@@ -1179,8 +1172,7 @@ describe("megolm-keys backup", () => {
};
}
},
{ overwriteRoutes: true },
);
});
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the new backup.
const newMessage: Partial<IEvent> = {
@@ -1216,7 +1208,7 @@ describe("megolm-keys backup", () => {
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
+20 -38
View File
@@ -16,7 +16,8 @@ limitations under the License.
import Olm from "@matrix-org/olm";
import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
import {
type IContent,
@@ -32,7 +33,6 @@ import { type ISyncResponder } from "../../test-utils/SyncResponder";
import { syncPromise } from "../../test-utils/test-utils";
import { type KeyBackupInfo } from "../../../src/crypto-api";
import { logger } from "../../../src/logger";
import type FetchMock from "fetch-mock";
/**
* @module
@@ -305,7 +305,9 @@ export function encryptMegolmEventRawPlainText(opts: {
},
type: "m.room.encrypted",
unsigned: {},
state_key: opts.plaintext.state_key ? `${opts.plaintext.type}:${opts.plaintext.state_key}` : undefined,
state_key: opts.plaintext.hasOwnProperty("state_key")
? `${opts.plaintext.type}:${opts.plaintext.state_key}`
: undefined,
};
}
@@ -460,19 +462,11 @@ export async function expectSendRoomKey(
return inboundGroupSession;
}
return await new Promise<Olm.InboundGroupSession>((resolve) => {
fetchMock.putOnce(
new RegExp("/sendToDevice/m.room.encrypted/"),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
resolve(onSendRoomKey(content));
return {};
},
{
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
overwriteRoutes: false,
},
);
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
const content = JSON.parse(callLog.options.body as string);
resolve(onSendRoomKey(content));
return {};
});
});
}
@@ -483,17 +477,11 @@ export async function expectSendRoomKey(
*/
export function expectEncryptedSendMessageEvent() {
return new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp("/send/m.room.encrypted/"),
(url, request) => {
const content = JSON.parse(request.body as string);
resolve(content);
return { event_id: "$event_id" };
},
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
{ overwriteRoutes: false },
);
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), (callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return { event_id: "$event_id" };
});
});
}
@@ -504,17 +492,11 @@ export function expectEncryptedSendMessageEvent() {
*/
function expectEncryptedSendStateEvent() {
return new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp("/state/m.room.encrypted/"),
(url, request) => {
const content = JSON.parse(request.body as string);
resolve(content);
return { event_id: "$event_id" };
},
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
{ overwriteRoutes: false },
);
fetchMock.putOnce(new RegExp("/state/m.room.encrypted/"), (callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return { event_id: "$event_id" };
});
});
}
+6 -8
View File
@@ -16,7 +16,7 @@ limitations under the License.
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { createClient, IndexedDBCryptoStore } from "../../../src";
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
@@ -26,7 +26,7 @@ import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostor
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
import { CryptoEvent } from "../../../src/crypto-api";
jest.setTimeout(15000);
vi.setConfig({ testTimeout: 15000 });
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -122,6 +122,7 @@ describe("MatrixClient.initRustCrypto", () => {
);
});
// eslint-disable-next-line @vitest/expect-expect
it("should ignore a second call", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -134,10 +135,6 @@ describe("MatrixClient.initRustCrypto", () => {
});
describe("Libolm Migration", () => {
beforeEach(() => {
fetchMock.reset();
});
it("should migrate from libolm", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
@@ -155,7 +152,7 @@ describe("MatrixClient.initRustCrypto", () => {
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
});
const progressListener = jest.fn();
const progressListener = vi.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
@@ -326,7 +323,7 @@ describe("MatrixClient.initRustCrypto", () => {
});
// When we start Rust crypto, potentially triggering an upgrade
const progressListener = jest.fn();
const progressListener = vi.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
@@ -478,6 +475,7 @@ describe("MatrixClient.clearStores", () => {
expect(await indexedDB.databases()).toHaveLength(0);
});
// eslint-disable-next-line @vitest/expect-expect
it("should not fail in environments without indexedDB", async () => {
// eslint-disable-next-line no-global-assign
indexedDB = undefined!;
+12 -10
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import Olm from "@matrix-org/olm";
@@ -23,7 +23,13 @@ import * as testUtils from "../../test-utils/test-utils";
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
import { TEST_ROOM_ID as ROOM_ID } from "../../test-utils/test-data";
import { logger } from "../../../src/logger";
import { createClient, PendingEventOrdering, type IStartClientOpts, type MatrixClient } from "../../../src/matrix";
import {
createClient,
HistoryVisibility,
PendingEventOrdering,
type IStartClientOpts,
type MatrixClient,
} from "../../../src/matrix";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
@@ -72,7 +78,6 @@ describe("Encrypted State Events", () => {
beforeEach(async () => {
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
@@ -96,15 +101,11 @@ describe("Encrypted State Events", () => {
}, 10000);
afterEach(async () => {
await aliceClient.stopClient();
await jest.runAllTimersAsync();
fetchMock.mockReset();
aliceClient.stopClient();
});
function expectAliceKeyQuery(response: any) {
fetchMock.postOnce(new RegExp("/keys/query"), (url: string, opts: RequestInit) => response, {
overwriteRoutes: false,
});
fetchMock.postOnce(new RegExp("/keys/query"), (callLog) => response);
}
function expectAliceKeyClaim(response: any) {
@@ -190,6 +191,7 @@ describe("Encrypted State Events", () => {
expect(decryptedEvent.getContent().topic).toEqual("Secret!");
});
// eslint-disable-next-line @vitest/expect-expect
it("Should send an encrypted state event", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -201,7 +203,7 @@ describe("Encrypted State Events", () => {
await startClientAndAwaitFirstSync();
// Alice shares a room with Bob
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], ROOM_ID, true));
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], HistoryVisibility.Joined, ROOM_ID, true));
await syncPromise(aliceClient);
// ... and claim one of Bob's OTKs ...
+1 -3
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import Olm from "@matrix-org/olm";
@@ -59,7 +59,6 @@ describe("to-device-messages", () => {
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://server.com";
aliceClient = createClient({
@@ -100,7 +99,6 @@ describe("to-device-messages", () => {
afterEach(async () => {
aliceClient.stopClient();
fetchMock.mockReset();
});
describe("encryptToDeviceMessages", () => {
+71 -92
View File
@@ -18,12 +18,12 @@ import "fake-indexeddb/auto";
import anotherjson from "another-json";
import debug from "debug";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto";
import Olm from "@matrix-org/olm";
import type FetchMock from "fetch-mock";
import {
createClient,
DebugLogger,
@@ -92,10 +92,7 @@ beforeAll(async () => {
}, 10000);
beforeEach(() => {
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
// to ensure that we don't end up with dangling timeouts.
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
});
afterEach(() => {
@@ -130,7 +127,6 @@ describe("verification", () => {
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
@@ -141,14 +137,12 @@ describe("verification", () => {
});
afterEach(async () => {
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}
aliceClient?.stopClient();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
if (vi.isFakeTimers()) {
await vi.runAllTimersAsync();
}
});
describe("Outgoing verification requests for another device", () => {
@@ -156,11 +150,10 @@ describe("verification", () => {
// pretend that we have another device, which we will verify
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`),
{ ok: false, status: 404 },
{ overwriteRoutes: true },
);
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`), {
ok: false,
status: 404,
});
});
// test with (1) the default verification method list, (2) a custom verification method list.
@@ -212,7 +205,7 @@ describe("verification", () => {
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
if (methods !== undefined) {
// eslint-disable-next-line jest/no-conditional-expect
// eslint-disable-next-line @vitest/no-conditional-expect
expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
}
@@ -245,7 +238,7 @@ describe("verification", () => {
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
vi.advanceTimersByTime(10);
requestBody = await sendToDevicePromise;
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
@@ -323,7 +316,7 @@ describe("verification", () => {
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// And now Alice starts a SAS verification
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
@@ -516,7 +509,7 @@ describe("verification", () => {
// Rust crypto waits for the 'done' to arrive from the other side.
if (request.phase === VerificationPhase.Done) {
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
// eslint-disable-next-line jest/no-conditional-expect
// eslint-disable-next-line @vitest/no-conditional-expect
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
await verificationPromise;
}
@@ -639,7 +632,7 @@ describe("verification", () => {
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// ... but Alice wants to do an SAS verification
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
@@ -684,7 +677,7 @@ describe("verification", () => {
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// ... but the dummy device wants to do an SAS verification
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
@@ -792,7 +785,7 @@ describe("verification", () => {
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
vi.advanceTimersByTime(10);
await sendToDevicePromise;
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
@@ -937,19 +930,16 @@ describe("verification", () => {
function awaitRoomMessageRequest(): Promise<IContent> {
return new Promise((resolve) => {
// Case of unencrypted message of the new crypto
fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
(url: string, options: RequestInit) => {
resolve(JSON.parse(options.body as string));
return { event_id: "$YUwRidLecu:example.com" };
},
);
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", (callLog) => {
resolve(JSON.parse(callLog.options.body as string));
return { event_id: "$YUwRidLecu:example.com" };
});
// Case of encrypted message of the old crypto
fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
async (url: string, options: RequestInit) => {
const encryptedMessage = JSON.parse(options.body as string);
async (callLog) => {
const encryptedMessage = JSON.parse(callLog.options.body as string);
const event = new MatrixEvent({
content: encryptedMessage,
type: "m.room.encrypted",
@@ -972,7 +962,7 @@ describe("verification", () => {
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
// 5ms by users, ie Bob and Alice
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
const messageRequestPromise = awaitRoomMessageRequest();
const verificationRequest = await aliceClient
@@ -1082,14 +1072,14 @@ describe("verification", () => {
});
it("ignores old verification requests", async () => {
const debug = jest.fn();
const info = jest.fn();
const warn = jest.fn();
const debug = vi.fn();
const info = vi.fn();
const warn = vi.fn();
// @ts-ignore overriding RustCrypto's logger
aliceClient.getCrypto()!.logger = { debug, info, warn };
const eventHandler = jest.fn();
const eventHandler = vi.fn();
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
const verificationRequestEvent = createVerificationRequestEvent();
@@ -1105,7 +1095,7 @@ describe("verification", () => {
// Wait until the request has been processed. We use a real sleep()
// here to make sure any background async tasks are completed.
jest.useRealTimers();
vi.useRealTimers();
await waitFor(async () => {
expect(info).toHaveBeenCalledWith(
expect.stringMatching(/^Ignoring just-received verification request/),
@@ -1187,7 +1177,7 @@ describe("verification", () => {
returnToDeviceMessageFromSync(toDeviceEvent);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// Wait for the request to be decrypted
const request1 = await requestEventPromise;
@@ -1224,7 +1214,7 @@ describe("verification", () => {
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// Advance time by 5mins, the verification request should be ignored after that
jest.advanceTimersByTime(5 * 60 * 1000);
vi.advanceTimersByTime(5 * 60 * 1000);
// Send Bob the room keys
returnToDeviceMessageFromSync(toDeviceEvent);
@@ -1290,7 +1280,7 @@ describe("verification", () => {
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
await syncPromise(aliceClient);
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// The client should now know about the olm device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
@@ -1302,11 +1292,10 @@ describe("verification", () => {
testOlmAccount?.free();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
await vi.runAllTimersAsync();
});
// eslint-disable-next-line @vitest/expect-expect
it("Should request cross signing keys after verification", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
@@ -1424,11 +1413,11 @@ describe("verification", () => {
*/
async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> {
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
vi.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
return aliceClient.getCrypto()!.getSessionBackupPrivateKey();
}
@@ -1461,27 +1450,21 @@ describe("verification", () => {
});
const expectBackupCheck = new Promise((resolve) => {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/version",
(url, request) => {
resolve(undefined);
if (expectBackup instanceof MatrixError) {
return {
status: expectBackup.httpStatus,
body: expectBackup.data,
};
}
fetchMock.get("express:/_matrix/client/v3/room_keys/version", (callLog) => {
resolve(undefined);
if (expectBackup instanceof MatrixError) {
return {
status: expectBackup.httpStatus,
body: expectBackup.data,
};
}
if (expectBackup instanceof Error) {
return Promise.reject(expectBackup);
}
if (expectBackup instanceof Error) {
return Promise.reject(expectBackup);
}
return expectBackup;
},
{
overwriteRoutes: true,
},
);
return expectBackup;
});
});
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
@@ -1562,7 +1545,7 @@ describe("verification", () => {
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
@@ -1596,8 +1579,8 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
return new Promise((resolve) => {
fetchMock.putOnce(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
resolve(JSON.parse(opts.body as string));
(callLog): RouteResponse => {
resolve(JSON.parse(callLog.options.body as string));
return {};
},
);
@@ -1618,29 +1601,25 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
const uskRequestResolvers = Promise.withResolvers<string>();
const backupKeyRequestResolvers = Promise.withResolvers<string>();
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const content = Object.values(messages)[0] as any;
if (content.action == "request") {
const name = content.name;
const requestId = content.request_id;
if (name == "m.cross_signing.user_signing") {
uskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.master") {
mskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.self_signing") {
sskRequestResolvers.resolve(requestId);
} else if (name == "m.megolm_backup.v1") {
backupKeyRequestResolvers.resolve(requestId);
}
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`), (callLog): RouteResponse => {
const messages = JSON.parse(callLog.options.body as string).messages[TEST_USER_ID];
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const content = Object.values(messages)[0] as any;
if (content.action == "request") {
const name = content.name;
const requestId = content.request_id;
if (name == "m.cross_signing.user_signing") {
uskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.master") {
mskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.self_signing") {
sskRequestResolvers.resolve(requestId);
} else if (name == "m.megolm_backup.v1") {
backupKeyRequestResolvers.resolve(requestId);
}
return {};
},
{ overwriteRoutes: true },
);
}
return {};
});
const promiseMap = new Map<string, Promise<string>>();
promiseMap.set("m.cross_signing.master", mskRequestResolvers.promise);
@@ -672,7 +672,7 @@ describe("MatrixClient event timelines", function () {
expect(timeline!.getEvents().find((e) => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
});
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
it("should return null when event is not in the thread that the given timelineSet is representing", () => {
// @ts-ignore
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
});
return Promise.all([
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeUndefined(),
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeNull(),
httpBackend.flushAllExpected(),
]);
});
it("should return undefined when event is within a thread but timelineSet is not", () => {
it("should return null when event is within a thread but timelineSet is not", () => {
// @ts-ignore
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
});
return Promise.all([
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeUndefined(),
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeNull(),
httpBackend.flushAllExpected(),
]);
});
@@ -2044,6 +2044,7 @@ describe("MatrixClient event timelines", function () {
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
}
// eslint-disable-next-line @vitest/expect-expect
it("in stable mode", async () => {
// @ts-ignore
client.clientOpts.threadSupport = true;
+10 -13
View File
@@ -347,6 +347,7 @@ describe("MatrixClient", function () {
expect((await prom).room_id).toBe(roomId);
});
// eslint-disable-next-line @vitest/expect-expect
it("should no-op if you've already knocked a room", function () {
const room = new Room(roomId, client, userId);
@@ -380,23 +381,16 @@ describe("MatrixClient", function () {
[
403,
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
"[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]",
],
[
500,
{ errcode: "INTERNAL_SERVER_ERROR" },
"[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]",
"MatrixError: [403] You don't have permission to knock",
],
[500, { errcode: "INTERNAL_SERVER_ERROR" }, "MatrixError: [500] Unknown message"],
];
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
const prom = client.knockRoom(roomId);
await Promise.all([
httpBackend.flushAllExpected(),
expect(prom).rejects.toMatchInlineSnapshot(snapshot),
]);
await Promise.all([httpBackend.flushAllExpected(), expect(prom).rejects.toThrow(snapshot)]);
});
});
});
@@ -1198,7 +1192,7 @@ describe("MatrixClient", function () {
describe("logout", () => {
it("should abort pending requests when called with stopClient=true", async () => {
httpBackend.when("POST", "/logout").respond(200, {});
const fn = jest.fn();
const fn = vi.fn();
client.http.request(Method.Get, "/test").catch(fn);
client.logout(true);
await httpBackend.flush(undefined);
@@ -1326,7 +1320,7 @@ describe("MatrixClient", function () {
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});
it("should always fetch capabilities and then cache", async () => {
@@ -1397,6 +1391,7 @@ describe("MatrixClient", function () {
});
describe("publicRooms", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should use GET request if no server or filter is specified", () => {
httpBackend.when("GET", "/publicRooms").respond(200, {});
client.publicRooms({});
@@ -1585,7 +1580,7 @@ describe("MatrixClient", function () {
describe("setSyncPresence", () => {
it("should pass calls through to the underlying sync api", () => {
const setPresence = jest.fn();
const setPresence = vi.fn();
// @ts-ignore
client.syncApi = { setPresence };
client.setSyncPresence(SetPresence.Unavailable);
@@ -1594,6 +1589,7 @@ describe("MatrixClient", function () {
});
describe("sendTyping", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should bail early for guests", async () => {
client.setGuest(true);
await client.sendTyping("!room:server", true, 100);
@@ -1851,6 +1847,7 @@ describe("MatrixClient", function () {
});
describe("setRoomMutePushRule", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should set room push rule to muted", async () => {
const roomId = "!roomId:server";
const client = new MatrixClient({
+1 -1
View File
@@ -159,7 +159,7 @@ describe("MatrixClient opts", function () {
await expect(
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
).rejects.toThrow("MatrixError: [500] Unknown message");
).rejects.toThrow("MatrixError: [500] Ruh roh");
});
it("shouldn't queue events", async () => {
@@ -720,7 +720,7 @@ describe("MatrixClient room timelines", function () {
} else {
reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire."));
}
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Vitest? */);
room.on(RoomEvent.TimelineReset, async () => {
try {
@@ -15,7 +15,7 @@ limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
@@ -83,8 +83,7 @@ describe("MatrixClient syncing errors", () => {
});
it("should retry, until errors are solved.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
vi.useFakeTimers();
fetchMock
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
@@ -105,19 +104,18 @@ describe("MatrixClient syncing errors", () => {
await client!.startClient();
expect(await syncEvents[0].promise).toBe(SyncState.Error);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[1].promise).toBe(SyncState.Error);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
});
it("should stop sync keep alive when client is stopped.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
vi.useFakeTimers();
fetchMock
.get("end:capabilities", {})
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
@@ -146,9 +144,9 @@ describe("MatrixClient syncing errors", () => {
const syntState = await firstSyncEvent.promise;
expect(syntState).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive
vi.runAllTimers(); // this will skip forward to trigger the keepAlive
jest.useRealTimers(); // we need real timer for the setTimout below to work
vi.useRealTimers(); // we need real timer for the setTimout below to work
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
+5 -4
View File
@@ -94,6 +94,7 @@ describe("MatrixClient syncing", () => {
presence: {},
};
// eslint-disable-next-line @vitest/expect-expect
it("should /sync after /pushrules and /filter.", async () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
@@ -501,7 +502,7 @@ describe("MatrixClient syncing", () => {
})
.respond(200, syncData);
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
client!.startClient({ initialSyncLimit: 1 });
return httpBackend!.flushAllExpected();
@@ -994,7 +995,7 @@ describe("MatrixClient syncing", () => {
roomVersion: "org.matrix.msc2716v3",
},
].forEach((testMeta) => {
// eslint-disable-next-line jest/valid-title
// eslint-disable-next-line @vitest/valid-title
describe(testMeta.label, () => {
const roomCreateEvent = utils.mkEvent({
type: "m.room.create",
@@ -1835,7 +1836,7 @@ describe("MatrixClient syncing", () => {
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
room!.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
room!.hasEncryptionStateEvent = vi.fn().mockReturnValue(true);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
@@ -2519,7 +2520,7 @@ describe("MatrixClient syncing", () => {
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
client!.store.storeAccountDataEvents([eventA1, eventB1]);
const fn = jest.fn();
const fn = vi.fn();
client!.on(ClientEvent.AccountData, fn);
httpBackend!.when("GET", "/sync").respond(200, {
@@ -62,7 +62,7 @@ describe("Notification count fixing", () => {
client!.startClient({ threadSupport: true });
const room = new Room(roomId, client!, selfUserId);
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
const event = new MatrixEvent({
room_id: roomId,
@@ -77,7 +77,7 @@ describe("Notification count fixing", () => {
},
});
jest.spyOn(event, "getPushActions").mockReturnValue({
vi.spyOn(event, "getPushActions").mockReturnValue({
notify: true,
tweaks: {},
});
@@ -123,7 +123,7 @@ describe("MatrixClient syncing", () => {
]);
const room = new Room(roomId, client!, selfUserId);
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!;
@@ -143,7 +143,7 @@ describe("MatrixClient syncing", () => {
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
let lastEvent: MatrixEvent | null = null;
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
vi.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
lastEvent = event as MatrixEvent;
return { event_id: reactionEventId };
});
@@ -195,7 +195,7 @@ describe("MatrixClient syncing", () => {
})
.respond(200, syncData);
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
client!.startClient({ initialSyncLimit: 1 });
await httpBackend!.flushAllExpected();
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
import { mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
import fetchMock from "@fetch-mock/vitest";
import {
MSC4108FailureReason,
@@ -40,7 +39,7 @@ import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
const baseUrl = "https://example.com";
const crypto = {
exportSecretsForQrLogin: jest.fn(),
exportSecretsForQrLogin: vi.fn(),
};
const client = {
doesServerSupportUnstableFeature(feature: string) {
@@ -54,9 +53,9 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
},
baseUrl,
getDomain: () => "example.com",
getDevice: jest.fn(),
getCrypto: jest.fn(() => crypto),
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
getDevice: vi.fn(),
getCrypto: vi.fn(() => crypto),
getAuthMetadata: vi.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
} as unknown as MatrixClient;
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
baseUrl: client.baseUrl,
@@ -77,10 +76,6 @@ describe("MSC4108SignInWithQR", () => {
});
});
afterEach(() => {
fetchMock.reset();
});
const url = "https://fallbackserver/rz/123";
const deviceId = "DEADB33F";
const verificationUri = "https://example.com/verify";
@@ -115,10 +110,10 @@ describe("MSC4108SignInWithQR", () => {
let opponentData = Promise.withResolvers<string>();
const ourMockSession = {
send: jest.fn(async (newData) => {
send: vi.fn(async (newData) => {
ourData.resolve(newData);
}),
receive: jest.fn(() => {
receive: vi.fn(() => {
const prom = opponentData.promise;
prom.then(() => {
opponentData = Promise.withResolvers();
@@ -134,10 +129,10 @@ describe("MSC4108SignInWithQR", () => {
},
} as unknown as MSC4108RendezvousSession;
const opponentMockSession = {
send: jest.fn(async (newData) => {
send: vi.fn(async (newData) => {
opponentData.resolve(newData);
}),
receive: jest.fn(() => {
receive: vi.fn(() => {
const prom = ourData.promise;
prom.then(() => {
ourData = Promise.withResolvers();
@@ -151,7 +146,7 @@ describe("MSC4108SignInWithQR", () => {
const ourChannel = new MSC4108SecureChannel(ourMockSession);
const qrCodeData = QrCodeData.fromBytes(
await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getDomain()!),
await ourChannel.generateCode(QrCodeIntent.Reciprocate, client.getDomain()!),
);
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
@@ -171,7 +166,7 @@ describe("MSC4108SignInWithQR", () => {
it("should be able to connect with opponent and share verificationUri", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
@@ -194,7 +189,7 @@ describe("MSC4108SignInWithQR", () => {
it("should abort if device already exists", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
@@ -244,12 +239,12 @@ describe("MSC4108SignInWithQR", () => {
// @ts-ignore
await opponentLogin.receive();
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
};
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
const payload = {
secrets: expect.objectContaining(secrets),
@@ -261,13 +256,13 @@ describe("MSC4108SignInWithQR", () => {
});
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
});
jest.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
vi.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + vi.mocked(setTimeout).mock.calls.length * 1000;
});
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
@@ -280,7 +275,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.send({
type: PayloadType.Success,
});
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
const ourProm = ourLogin.shareSecrets();
await expect(ourProm).rejects.toThrow("New device not found");
@@ -297,7 +292,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.send({
type: PayloadType.Success,
});
mocked(client.getDevice).mockRejectedValue(
vi.mocked(client.getDevice).mockRejectedValue(
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
);
@@ -314,7 +309,7 @@ describe("MSC4108SignInWithQR", () => {
});
it("should not send secrets if user cancels", async () => {
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
@@ -334,7 +329,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.receive();
const deviceResolvers = Promise.withResolvers<IMyDevice>();
mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
vi.mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
deviceResolvers.resolve({} as IMyDevice);
@@ -342,7 +337,7 @@ describe("MSC4108SignInWithQR", () => {
const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
};
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
await Promise.all([
expect(ourProm).rejects.toThrow("User cancelled"),
+21 -19
View File
@@ -26,7 +26,7 @@ import {
type Extension,
} from "../../src/sliding-sync";
import { TestClient } from "../TestClient";
import { type IRoomEvent, type IStateEvent } from "../../src";
import { type IContent, type IRoomEvent, type IStateEvent } from "../../src";
import {
type MatrixClient,
type MatrixEvent,
@@ -68,17 +68,17 @@ describe("SlidingSyncSdk", () => {
const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getListParams = jest.fn();
s.getListData = jest.fn();
s.getRoomSubscriptions = jest.fn();
s.modifyRoomSubscriptionInfo = jest.fn();
s.modifyRoomSubscriptions = jest.fn();
s.registerExtension = jest.fn();
s.setList = jest.fn();
s.setListRanges = jest.fn();
s.start = jest.fn();
s.stop = jest.fn();
s.resend = jest.fn();
s.getListParams = vi.fn();
s.getListData = vi.fn();
s.getRoomSubscriptions = vi.fn();
s.modifyRoomSubscriptionInfo = vi.fn();
s.modifyRoomSubscriptions = vi.fn();
s.registerExtension = vi.fn();
s.setList = vi.fn();
s.setListRanges = vi.fn();
s.start = vi.fn();
s.stop = vi.fn();
s.resend = vi.fn();
return s;
};
@@ -111,7 +111,7 @@ describe("SlidingSyncSdk", () => {
expect(m.getType()).toEqual(want[i].type);
expect(m.getSender()).toEqual(want[i].sender);
expect(m.getId()).toEqual(want[i].event_id);
expect(m.getContent()).toEqual(want[i].content);
expect(m.getContent<IContent>()).toEqual(want[i].content);
expect(m.getTs()).toEqual(want[i].origin_server_ts);
if (want[i].unsigned) {
expect(m.getUnsigned()).toEqual(want[i].unsigned);
@@ -150,7 +150,7 @@ describe("SlidingSyncSdk", () => {
// find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension<any, any> => {
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
const mockFn = vi.mocked(mockSlidingSync!.registerExtension);
// find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
@@ -658,7 +658,7 @@ describe("SlidingSyncSdk", () => {
});
it("can update device lists", () => {
syncCryptoCallback!.processDeviceLists = jest.fn();
syncCryptoCallback!.processDeviceLists = vi.fn();
ext.onResponse({
device_lists: {
changed: ["@alice:localhost"],
@@ -672,7 +672,7 @@ describe("SlidingSyncSdk", () => {
});
it("can update OTK counts and unused fallback keys", () => {
syncCryptoCallback!.processKeyCounts = jest.fn();
syncCryptoCallback!.processKeyCounts = vi.fn();
ext.onResponse({
device_one_time_keys_count: {
signed_curve25519: 42,
@@ -722,7 +722,7 @@ describe("SlidingSyncSdk", () => {
});
globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeTruthy();
expect(globalData.getContent()).toEqual(globalContent);
expect(globalData.getContent<IContent>()).toEqual(globalContent);
});
it("processes rooms account data", async () => {
@@ -757,7 +757,7 @@ describe("SlidingSyncSdk", () => {
expect(room).toBeTruthy();
const event = room.getAccountData(roomType)!;
expect(event).toBeTruthy();
expect(event.getContent()).toEqual(roomContent);
expect(event.getContent<IContent>()).toEqual(roomContent);
});
it("doesn't crash for unknown room account data", async () => {
@@ -847,6 +847,7 @@ describe("SlidingSyncSdk", () => {
});
});
// eslint-disable-next-line @vitest/expect-expect
it("can handle missing fields", async () => {
ext.onResponse({
next_batch: "23456",
@@ -861,7 +862,7 @@ describe("SlidingSyncSdk", () => {
};
let called = false;
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
expect(ev.getContent()).toEqual(toDeviceContent);
expect(ev.getContent<IContent>()).toEqual(toDeviceContent);
expect(ev.getType()).toEqual(toDeviceType);
called = true;
});
@@ -1095,6 +1096,7 @@ describe("SlidingSyncSdk", () => {
expect(receipt?.data.thread_id).toBeFalsy();
});
// eslint-disable-next-line @vitest/expect-expect
it("gracefully handles missing rooms when receiving receipts", async () => {
const roomId = "!room:id";
const alice = "@alice:alice";
+1
View File
@@ -82,6 +82,7 @@ describe("SlidingSync", () => {
await p;
});
// eslint-disable-next-line @vitest/expect-expect
it("should stop the sync loop upon calling stop()", () => {
slidingSync.stop();
httpBackend!.verifyNoOutstandingExpectation();
+13 -4
View File
@@ -14,13 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
jest.mock("../src/http-api/utils", () => ({
...jest.requireActual("../src/http-api/utils"),
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
vi.mock("../src/http-api/utils", async () => ({
...(await vi.importActual("../src/http-api/utils")),
// We mock timeoutSignal otherwise it causes tests to leave timers running
timeoutSignal: () => new AbortController().signal,
}));
// Dont make test fail too soon due to timeouts while debugging.
manageFetchMockGlobally();
beforeEach(() => {
fetchMock.hardReset();
fetchMock.mockGlobal();
});
// Don't make test fail too soon due to timeouts while debugging.
if (process.env.VSCODE_INSPECTOR_OPTIONS) {
jest.setTimeout(60 * 1000 * 5); // 5 minutes
vi.setConfig({ testTimeout: 60 * 1000 * 5 }); // 5 minutes
}
+60 -45
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type ISyncResponder } from "./SyncResponder";
@@ -36,65 +36,80 @@ export class AccountDataAccumulator {
public constructor(private syncResponder: ISyncResponder) {}
private accountDataResolvers = new Map<string, PromiseWithResolvers<any>>();
private setInterceptRunning = false;
/**
* Intercept requests to set a particular type of account data.
* Intercept setting of account data.
*
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
* resolved.
*
* @param accountDataType - type of account data to be intercepted
* @param opts - options to pass to fetchMock
* @returns a Promise which will resolve (with the content of the account data) once it is set.
*/
public interceptSetAccountData(
accountDataType: string,
opts?: Parameters<(typeof fetchMock)["put"]>[2],
): Promise<any> {
return new Promise((resolve) => {
// Called when the cross signing key is uploaded
fetchMock.put(
`express:/_matrix/client/v3/user/:userId/account_data/${accountDataType}`,
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
const type = url.split("/").pop();
// update account data for sync response
this.accountDataEvents.set(type!, content);
resolve(content);
public interceptSetAccountData(): void {
if (this.setInterceptRunning) return;
this.setInterceptRunning = true;
// return a sync response
this.sendSyncResponseWithUpdatedAccountData();
return {};
},
opts,
);
fetchMock.put(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
const content = JSON.parse(callLog.options.body as string);
const type = callLog.url.split("/").pop();
// update account data for sync response
this.accountDataEvents.set(type!, content);
this.accountDataResolvers.get(type!)?.resolve(content);
if (!this.accountDataResolvers.delete(type!)) {
// Check for a wildcard matcher
for (const [key, resolver] of this.accountDataResolvers) {
if (key.endsWith("*") && type?.startsWith(key.slice(0, -1))) {
resolver.resolve(content);
this.accountDataResolvers.delete(key);
}
}
}
// return a sync response
this.sendSyncResponseWithUpdatedAccountData();
return {};
});
}
/**
* Wait for a particular type of account data.
*
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
* resolved.
*
* @returns a Promise which will resolve (with the content of the account data) once it is set.
*/
public waitForAccountData(type: string): Promise<any> {
const resolvers = Promise.withResolvers<any>();
this.accountDataResolvers.set(type, resolvers);
this.interceptSetAccountData();
return resolvers.promise;
}
/**
* Intercept all requests to get account data
*/
public interceptGetAccountData(): void {
fetchMock.get(
`express:/_matrix/client/v3/user/:userId/account_data/:type`,
(url) => {
const type = url.split("/").pop();
const existing = this.accountDataEvents.get(type!);
if (existing) {
// return it
return {
status: 200,
body: existing,
};
} else {
// 404
return {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
};
}
},
{ overwriteRoutes: true },
);
fetchMock.get(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
const type = callLog.url.split("/").pop();
const existing = this.accountDataEvents.get(type!);
if (existing) {
// return it
return {
status: 200,
body: existing,
};
} else {
// 404
return {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
};
}
});
}
/**
+10 -9
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import debugFunc, { type Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
import type { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src";
@@ -81,26 +81,27 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
// set up a listener for /keys/upload.
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
const listener = (url: string, options: RequestInit) =>
this.onKeyUploadRequest(resolveOneTimeKeys, options);
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
fetchMock.post(
new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(),
(callLog) => this.onKeyUploadRequest(resolveOneTimeKeys, callLog.options),
{ name: routeNamePrefix + "keys-upload" },
);
});
fetchMock.post(
new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
(callLog) => this.onSignaturesUploadRequest(callLog.options),
{
url: new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
name: routeNamePrefix + "upload-sigs",
},
(url, options) => this.onSignaturesUploadRequest(options),
);
fetchMock.post(
new URL("/_matrix/client/v3/keys/device_signing/upload", homeserverUrl).toString(),
(callLog) => this.onSigningKeyUploadRequest(callLog.options),
{
url: new URL("/_matrix/client/v3/keys/device_signing/upload", homeserverUrl).toString(),
name: routeNamePrefix + "upload-cross-signing-keys",
},
(url, options) => this.onSigningKeyUploadRequest(options),
);
}
+4 -3
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { MapWithDefault } from "../../src/utils";
import { type IDownloadKeyResult, type SigningKeys } from "../../src";
@@ -42,8 +42,9 @@ export class E2EKeyResponder {
*/
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);
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), (callLog) =>
this.onKeyQueryRequest(callLog.options),
);
}
private onKeyQueryRequest(options: RequestInit) {
+4 -3
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { MapWithDefault } from "../../src/utils";
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
@@ -36,8 +36,9 @@ export class E2EOTKClaimResponder {
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
const listener = (url: string, options: RequestInit) => this.onKeyClaimRequest(options);
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), listener);
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), (callLog) =>
this.onKeyClaimRequest(callLog.options),
);
}
private onKeyClaimRequest(options: RequestInit) {
+4 -5
View File
@@ -16,9 +16,8 @@ limitations under the License.
import debugFunc from "debug";
import { type Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import type FetchMock from "fetch-mock";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
/** Interface implemented by classes that intercept `/sync` requests from test clients
*
@@ -76,12 +75,12 @@ export class SyncResponder implements ISyncResponder {
*/
public constructor(homeserverUrl: string) {
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (callLog) =>
this.onSyncRequest(),
);
}
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
private async onSyncRequest(): Promise<RouteResponse> {
switch (this.state) {
case SyncResponderState.IDLE: {
this.debug("Got /sync request: waiting for response to be ready");
+23 -17
View File
@@ -14,12 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MethodLikeKeys, mocked, type MockedObject } from "jest-mock";
import { type MockedObject } from "vitest";
import { type ClientEventHandlerMap, type EmittedEvents, type MatrixClient } from "../../src/client";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { User } from "../../src/models/user";
// Cribbed from https://github.com/jestjs/jest/blob/94830794dc5dfca1b49bc435b7b031b27838a798/packages/jest-mock/src/index.ts
type FunctionLike = (...args: any) => any;
type MethodLikeKeys<T> = keyof {
[K in keyof T as Required<T>[K] extends FunctionLike ? K : never]: T[K];
};
/**
* Mock client with real event emitter
* useful for testing code that listens
@@ -34,19 +40,19 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
/**
* - make a mock client
* - cast the type to mocked(MatrixClient)
* - cast the type to vi.mocked(MatrixClient)
* - spy on MatrixClientPeg.get to return the mock
* eg
* ```
* const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId),
getUserId: vi.fn().mockReturnValue(aliceId),
});
* ```
*/
export const getMockClientWithEventEmitter = (
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
): MockedObject<MatrixClient> => {
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
const mock = vi.mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
return mock;
};
@@ -59,14 +65,14 @@ export const getMockClientWithEventEmitter = (
* ```
*/
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
getUserId: jest.fn().mockReturnValue(userId),
getSafeUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
getUserId: vi.fn().mockReturnValue(userId),
getSafeUserId: vi.fn().mockReturnValue(userId),
getUser: vi.fn().mockReturnValue(new User(userId)),
isGuest: vi.fn().mockReturnValue(false),
mxcUrlToHttp: vi.fn().mockReturnValue("mock-mxcUrlToHttp"),
credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
getThreePids: vi.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: vi.fn(),
});
/**
@@ -78,16 +84,16 @@ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
* ```
*/
export const mockClientMethodsEvents = () => ({
decryptEventIfNeeded: jest.fn(),
getPushActionsForEvent: jest.fn(),
decryptEventIfNeeded: vi.fn(),
getPushActionsForEvent: vi.fn(),
});
/**
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCachedCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
getIdentityServerUrl: vi.fn(),
getHomeserverUrl: vi.fn(),
getCachedCapabilities: vi.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false),
});
+4 -2
View File
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MockInstance } from "vitest";
/**
* Filter emitter.emit mock calls to find relevant events
* eg:
* ```
* const emitSpy = jest.spyOn(state, 'emit');
* const emitSpy = vi.spyOn(state, 'emit');
* << actions >>
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
* expect(beaconLivenessEmits.length).toBe(1);
* ```
*/
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
export const filterEmitCallsByEventType = (eventType: string, spy: MockInstance<(...args: any[]) => any>) =>
spy.mock.calls.filter((args) => args[0] === eventType);
+4 -6
View File
@@ -14,12 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
// Vitest lacks tickAsync() and a number of other async methods which break the event loop,
// letting scheduled promise callbacks run. So we have to do it manually
// (this is what sinon does under the hood). We do both in a loop until the thing we expect happens:
// hopefully this is the least flakey way and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
export function flushPromises() {
return new Promise((r) => {
+31 -30
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type KeyBackupInfo } from "../../src/crypto-api";
@@ -25,20 +25,15 @@ import { type KeyBackupInfo } from "../../src/crypto-api";
* @param userId - the local user's ID. Defaults to `@alice:localhost`.
*/
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
fetchMock.getOnce(
new URL("/_matrix/client/versions", homeserverUrl).toString(),
{ versions: ["v1.1"] },
{ overwriteRoutes: true },
);
fetchMock.getOnce(
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
{},
{ overwriteRoutes: true },
);
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/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(),
{ filter_id: "fid" },
{ overwriteRoutes: true },
);
fetchMock.getOnce(
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter/fid`, homeserverUrl).toString(),
{ filter_id: "fid" },
);
}
@@ -65,24 +60,30 @@ export function mockSetupCrossSigningRequests(): void {
* @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.get(
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No current backup version",
},
},
});
{ name: "room-keys-version" },
);
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
overwriteRoutes: true,
});
return {
version: backupVersion,
};
});
fetchMock.post(
"path:/_matrix/client/v3/room_keys/version",
(callLog) => {
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
fetchMock.modifyRoute("room-keys-version", { response: backupData });
return {
version: backupVersion,
};
},
{ name: "post-room-keys-version" },
);
}
+26 -17
View File
@@ -84,9 +84,9 @@ def main() -> None:
* 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";
import type {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
import type {{ IDownloadKeyResult, IEvent }} from "../../../src";
import type {{ KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions }} from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
@@ -246,15 +246,6 @@ export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult>
/** 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)
@@ -278,6 +269,23 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, 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) };
/**
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}`.
* Contains the key from {prefix}MEGOLM_SESSION_DATA.
*/
export const {prefix}PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {{
[{prefix}MEGOLM_SESSION_DATA.session_id]: {prefix}CURVE25519_KEY_BACKUP_DATA
}};
"""
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
@@ -385,7 +393,7 @@ def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
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.
that can be imported via importRoomKeys API, or shared via MSC4268 room history sharing.
Returns the exported key, the matching privat edKey (needed to encrypt)
"""
index = 0
@@ -409,11 +417,12 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
"session_id": encode_base64(
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
),
"session_key": encode_base64(exported_key),
"session_key": encode_base64(bytes(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": [],
"m.shared_history": True,
}
return megolm_export, private_key
@@ -458,7 +467,7 @@ def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed2
"room_id": "!room:id",
"sender_key": previous["sender_key"],
"session_id": previous["session_id"],
"session_key": encode_base64(exported_key),
"session_key": encode_base64(bytes(exported_key)),
"sender_claimed_keys": previous["sender_claimed_keys"],
"forwarding_curve25519_key_chain": [],
}
@@ -609,7 +618,7 @@ def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519
message += signature
cipher_text = encode_base64(message)
cipher_text = encode_base64(bytes(message))
encrypted_payload = {
"algorithm" : "m.megolm.v1.aes-sha2",
@@ -653,7 +662,7 @@ def export_recovery_key(key_b64: str) -> str:
export_bytes += parity_byte.to_bytes(1, 'big')
# The byte string is encoded using base58
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
recovery_key = base58.b58encode(bytes(export_bytes)).decode('utf-8')
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
return ' '.join(split)
+76 -51
View File
@@ -3,9 +3,9 @@
* Do not edit by hand! This file is generated by `./generate-test-data.py`
*/
import { type IDeviceKeys, type IMegolmSessionData } from "../../../src/@types/crypto";
import { type IDownloadKeyResult, type IEvent } from "../../../src";
import { type KeyBackupSession, type KeyBackupInfo } from "../../../src/crypto-api/keybackup";
import type { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
import type { IDownloadKeyResult, IEvent } from "../../../src";
import type { KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions } from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
@@ -118,26 +118,6 @@ export const ONE_TIME_KEYS = {
}
};
/** 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[] = [
{
@@ -149,7 +129,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": {
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
},
{
"algorithm": "m.megolm.v1.aes-sha2",
@@ -160,7 +141,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": {
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
}
];
@@ -174,7 +156,8 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
"sender_claimed_keys": {
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
};
/** A ratcheted version of MEGOLM_SESSION_DATA */
@@ -196,7 +179,7 @@ export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"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",
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d/r0NTff1SQt+1GopZkT0nq6jF5Wh/oX+8iwtYjHvTxMpN1UQoXAvRF40O+EVg+Q3efJXh1t45cMco8EWU64VerOir+k7cQ3C9FtcgQw3kmz3s3HeVY10o13X/w6+rc8n6vXqxuIxYHnFxanxX8B6TgTMZNajNfVsmJV0aC1aezim7E2gsftc+6+zW5G+rCFaEsWV/IuSOUz0+Hh0U+7hzSrz9/4qXPEVmPy1f6Ll4hhquPAlXPVDwddqlJDYj7kmvzr1g3bKVpk+TtKDbWlVQDPaJx2DEI2jGkPYjhYb7okpTFKpUny94dZmFIQqCeSGPIniaq8Y+/CanugQ1ZRVQcThuXrTewqWhXcpVvkVHT9i4ImcpBl95HzCBXuiwSUv6FKvO25fp++w555rbn2piFtilrUwnkrZPW32jFuaQcKZF4mZwcLeH7POL5UCuS4TWyaKyArp7bRzXwWuIq1wPET2nAMUmUVL7ge2+tAevk1WOIsjLgSaz/g55wO3Yma7yhXRFKcnzTjS0hUQOZ3GfTNwCM4pjzAtIPzvVd4Fp0b1emWZS5WyOYdXsceEDi3c6WtkoHWOKhPU0zBzn8hA9TdlFFqKzf2QFbN5Zgg0gprDLnLWgpc3/ieI4C7ndEQ7ZeTNMXbT/Y10APFk3qO+IGkLXJ97/qTF41EXFDhlsL0",
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
"mac": "OibmACbORhI"
}
@@ -229,6 +212,37 @@ export const ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000
};
/** base64-encoded backup decryption (private) key that matches the public key in CURVE25519_KEY_BACKUP_DATA */
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** base64-encoded backup decryption (private) key that does not match the public key in CURVE25519_KEY_BACKUP_DATA */
export const BACKUP_DECRYPTION_KEY_BASE64_ALT = "dh4fP2LITyJusgnb0dEq/SQK253WGObvLxXF5FEX6qc";
/** 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"
}
}
}
};
/**
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
* Contains the key from MEGOLM_SESSION_DATA.
*/
export const PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
[MEGOLM_SESSION_DATA.session_id]: CURVE25519_KEY_BACKUP_DATA
};
// Bob data
export const BOB_TEST_USER_ID = "@bob:xyz";
@@ -338,26 +352,6 @@ export const BOB_ONE_TIME_KEYS = {
}
};
/** 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[] = [
{
@@ -369,7 +363,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": {
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
},
{
"algorithm": "m.megolm.v1.aes-sha2",
@@ -380,7 +375,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": {
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
}
];
@@ -394,7 +390,8 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"sender_claimed_keys": {
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
},
"forwarding_curve25519_key_chain": []
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
};
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
@@ -416,7 +413,7 @@ export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"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",
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAA7TEpHlx8Ks23hCqXmVW710VjqK2K9xnWCyJvkHfE8x0w6AYvffDj+tRVP8C8M7t4849rD2itn0uma+YMkvjG/nANUTxG1dBf3oUOZ673vflCPoaz7s7x9ZNhYDVSVH5JTdMgNwwN42R5dqqxnGTu516tJzJh/9BWvyD9oIPWJ8X0rt1sbzEJ3PZeBXcSy8GTlZ1SgSFjeiXlwYxOZCaX2sxprk4N1oI1db6g+wCDBhbCGGucJIlTDJna/h9/C5J4drGd/fkisG3SidUmJXXCyInhs/BhwjGAtTGeQS8j7R8UnJxhMulYBHSckzj0Kas71LElPp8W8M4Jq81APA03n5UfYB+U6jbxjDgf8OJnxGQyrteq9F2+SEvS/TwHe1pE3t6EM2mDYRoYDTpU5pTNYSJkGIQMfWJKRxxuWUGs29o1twewJ6dhHgm+SlCII0M7ESoVdV54vxZCvHZnPcR0NXDzal7ils7zBKJmamHfPQBuaqNPU3KmSo+5R8ngFPaWU5LbWqYp/WxSBfNCoLZ7Jf8Io5uitjXTATR2qy2r6l/RJmk3RlfP51kliQqI2TWqRF96oaB96IGgUGSFCX/2pv0psOBGc1SjfmMB3d7gYis+2iBYVbG3xmnpeXbqvlD0Lw9TiTIPkjhJkTW1+lXyhy1xVH9ZmcFamcL7bX15Jx",
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
"mac": "lEfHlqfJQwU"
}
@@ -449,6 +446,34 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000
};
/** 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"
}
}
}
};
/**
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
* Contains the key from BOB_MEGOLM_SESSION_DATA.
*/
export const BOB_PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
[BOB_MEGOLM_SESSION_DATA.session_id]: BOB_CURVE25519_KEY_BACKUP_DATA
};
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
+22 -8
View File
@@ -13,6 +13,7 @@ import {
import {
ClientEvent,
EventType,
HistoryVisibility,
type IJoinedRoom,
type IPusher,
type ISyncResponse,
@@ -57,14 +58,19 @@ 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
* Return a sync response which contains a single room (by default `TEST_ROOM_ID`), with the members given
* and history visibility set to `shared`.
*
* @returns the sync response
* @param roomMembers - An array of user IDs representing the members of the room.
* @param roomHistoryVisibility - The history visibility setting for the room. Defaults to `shared`.
* @param roomId - The ID of the room. Defaults to `TEST_ROOM_ID`.
* @param encryptStateEvents - A boolean indicating whether state events should be encrypted. Defaults to `false`.
*
* @returns The sync response object containing the room data.
*/
export function getSyncResponse(
roomMembers: string[],
roomHistoryVisibility: HistoryVisibility = HistoryVisibility.Shared,
roomId = TEST_ROOM_ID,
encryptStateEvents = false,
): ISyncResponse {
@@ -82,7 +88,15 @@ export function getSyncResponse(
state_key: "",
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"io.element.msc3414.encrypt_state_events": encryptStateEvents,
"io.element.msc4362.encrypt_state_events": encryptStateEvents,
},
}),
mkEventCustom({
sender: roomMembers[0],
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: roomHistoryVisibility,
},
}),
],
@@ -136,7 +150,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
// eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
result[key] = vi.fn();
}
} catch {
// Direct access to some non-function fields of DOM prototypes may
@@ -592,7 +606,7 @@ export async function advanceTimersUntil<T>(promise: Promise<T>): Promise<T> {
});
while (!resolved) {
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
}
return await promise;
@@ -641,7 +655,7 @@ export function waitFor<T>(
checkCallback();
while (!finished) {
jest.advanceTimersByTime(interval);
vi.advanceTimersByTime(interval);
// Could have timed-out
if (finished) break;
+51 -92
View File
@@ -17,21 +17,17 @@ limitations under the License.
import {
type ClientEvent,
type ClientEventHandlerMap,
type EmptyObject,
EventType,
type GroupCall,
GroupCallIntent,
GroupCallType,
type IContent,
type ISendEventResponse,
type MatrixClient,
type MatrixEvent,
type Room,
RoomMember,
type RoomState,
RoomStateEvent,
type RoomStateEventHandlerMap,
type SendToDeviceContentMap,
} from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
@@ -269,19 +265,20 @@ export class MockRTCRtpTransceiver {
this.peerConn.needsNegotiation = true;
}
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
public setCodecPreferences = vi.fn<RTCRtpTransceiver["setCodecPreferences"]>();
}
export class MockMediaStreamTrack {
export class MockMediaStreamTrack extends EventTarget {
constructor(
public readonly id: string,
public readonly kind: "audio" | "video",
public enabled = true,
) {}
) {
super();
}
public stop = jest.fn<void, []>();
public stop = vi.fn<() => void>();
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public settings?: MediaTrackSettings;
@@ -289,45 +286,21 @@ export class MockMediaStreamTrack {
return this.settings!;
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
public typed(): MediaStreamTrack {
return this as unknown as MediaStreamTrack;
}
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
export class MockMediaStream {
export class MockMediaStream extends EventTarget {
constructor(
public id: string,
private tracks: MockMediaStreamTrack[] = [],
) {}
) {
super();
}
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public getTracks() {
return this.tracks;
}
@@ -337,17 +310,9 @@ export class MockMediaStream {
public getVideoTracks() {
return this.tracks.filter((track) => track.kind === "video");
}
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
public addTrack(track: MockMediaStreamTrack) {
this.tracks.push(track);
this.dispatchEvent("addtrack");
this.dispatchEvent(new Event("addtrack"));
}
public removeTrack(track: MockMediaStreamTrack) {
this.tracks.splice(this.tracks.indexOf(track), 1);
@@ -391,7 +356,7 @@ export class MockMediaHandler {
public stopUserMediaStream(stream: MockMediaStream) {
stream.isStopped = true;
}
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
public getScreensharingStream = vi.fn((opts?: IScreensharingOpts) => {
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
@@ -416,19 +381,19 @@ export class MockMediaHandler {
}
export class MockMediaDevices {
public enumerateDevices = jest
.fn<Promise<MediaDeviceInfo[]>, []>()
public enumerateDevices = vi
.fn<MediaDevices["enumerateDevices"]>()
.mockResolvedValue([
new MockMediaDeviceInfo("audioinput").typed(),
new MockMediaDeviceInfo("videoinput").typed(),
]);
public getUserMedia = jest
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
public getUserMedia = vi
.fn<MediaDevices["getUserMedia"]>()
.mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
public getDisplayMedia = jest
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
public getDisplayMedia = vi
.fn<MediaDevices["getDisplayMedia"]>()
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
public typed(): MediaDevices {
@@ -462,14 +427,8 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
calls: new Map<string, MatrixCall>(),
};
public sendStateEvent = jest.fn<
Promise<ISendEventResponse>,
[roomId: string, eventType: EventType, content: any, statekey: string]
>();
public sendToDevice = jest.fn<
Promise<EmptyObject>,
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
>();
public sendStateEvent = vi.fn<MatrixClient["sendStateEvent"]>();
public sendToDevice = vi.fn<MatrixClient["sendToDevice"]>();
public isInitialSyncComplete(): boolean {
return false;
@@ -499,11 +458,11 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getUseE2eForGroupCall = () => false;
public checkTurnServers = () => null;
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
public getSyncState = vi.fn<MatrixClient["getSyncState"]>().mockReturnValue(SyncState.Syncing);
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();
public getFoci = jest.fn();
public getRooms = vi.fn<MatrixClient["getRooms"]>().mockReturnValue([]);
public getRoom = vi.fn();
public getFoci = vi.fn();
public supportsThreads(): boolean {
return true;
@@ -534,20 +493,20 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
isAudioMuted: jest.fn().mockReturnValue(false),
isVideoMuted: jest.fn().mockReturnValue(false),
setAudioVideoMuted: vi.fn<CallFeed["setAudioVideoMuted"]>(),
isAudioMuted: vi.fn().mockReturnValue(false),
isVideoMuted: vi.fn().mockReturnValue(false),
stream: new MockMediaStream("stream"),
} as unknown as CallFeed;
public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
public hangup = jest.fn<void, []>();
public initStats = jest.fn<void, []>();
public reject = vi.fn<() => void>();
public answerWithCallFeeds = vi.fn<MatrixCall["answerWithCallFeeds"]>();
public hangup = vi.fn<() => void>();
public initStats = vi.fn<() => void>();
public sendMetadataUpdate = jest.fn<void, []>();
public sendMetadataUpdate = vi.fn<() => void>();
public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember;
@@ -586,11 +545,11 @@ export class MockCallFeed {
}
export function installWebRTCMocks() {
globalThis.navigator = {
vi.stubGlobal("navigator", {
mediaDevices: new MockMediaDevices().typed(),
} as unknown as Navigator;
});
globalThis.window = {
vi.stubGlobal("window", {
// @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock
@@ -598,16 +557,16 @@ export function installWebRTCMocks() {
// @ts-ignore Mock
RTCIceCandidate: {},
getUserMedia: () => new MockMediaStream("local_stream"),
};
// @ts-ignore Mock
globalThis.document = {};
});
vi.stubGlobal("document", {});
// @ts-ignore Mock
globalThis.AudioContext = MockAudioContext;
// @ts-ignore Mock
globalThis.RTCRtpReceiver = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
getCapabilities: vi.fn().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
@@ -615,7 +574,7 @@ export function installWebRTCMocks() {
// @ts-ignore Mock
globalThis.RTCRtpSender = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
getCapabilities: vi.fn().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
@@ -632,22 +591,22 @@ export function makeMockGroupCallStateEvent(
redacted?: boolean,
): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue(content),
getStateKey: jest.fn().mockReturnValue(groupCallId),
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
getType: vi.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: vi.fn().mockReturnValue(roomId),
getTs: vi.fn().mockReturnValue(0),
getContent: vi.fn().mockReturnValue(content),
getStateKey: vi.fn().mockReturnValue(groupCallId),
isRedacted: vi.fn().mockReturnValue(redacted ?? false),
} as unknown as MatrixEvent;
}
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue({}),
getStateKey: jest.fn().mockReturnValue(groupCallId),
getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getRoomId: vi.fn().mockReturnValue(roomId),
getTs: vi.fn().mockReturnValue(0),
getContent: vi.fn().mockReturnValue({}),
getStateKey: vi.fn().mockReturnValue(groupCallId),
} as unknown as MatrixEvent;
}
+2 -2
View File
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
});
it("should have a falsey unstable if needed", () => {
const ns = new NamespacedValue("stable");
const ns = new NamespacedValue("stable", null);
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.stable]);
@@ -61,7 +61,7 @@ describe("UnstableValue", () => {
it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null!, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(<any>ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
});
+2 -2
View File
@@ -38,7 +38,7 @@ describe("ReEmitter", function () {
const src = new EventSource();
const tgt = new EventTarget();
const handler = jest.fn();
const handler = vi.fn();
tgt.on(EVENTNAME, handler);
const reEmitter = new ReEmitter(tgt);
@@ -61,7 +61,7 @@ describe("ReEmitter", function () {
// without the workaround in ReEmitter, this would throw
src.doAnError();
const handler = jest.fn();
const handler = vi.fn();
tgt.on("error", handler);
src.doAnError();
+3 -3
View File
@@ -35,16 +35,16 @@ describe("onResumedSync", () => {
};
store = new StubStore();
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
store.getOldestToDeviceBatch = vi.fn().mockImplementation(() => {
return batch;
});
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
store.removeToDeviceBatch = vi.fn().mockImplementation(() => {
batch = null;
});
mockClient = getMockClientWithEventEmitter({});
mockClient.store = store;
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
mockClient.sendToDevice = vi.fn().mockImplementation(async () => {
if (shouldFailSendToDevice) {
await Promise.reject(new ConnectionError("")).finally(() => {
setTimeout(onSendToDeviceFailure, 0);
+2 -1
View File
@@ -35,6 +35,7 @@ describe("AutoDiscovery", function () {
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
});
// eslint-disable-next-line @vitest/expect-expect
it("should throw an error when no domain is specified", function () {
getHttpBackend();
return Promise.all([
@@ -190,7 +191,7 @@ describe("AutoDiscovery", function () {
};
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then(expect(expected).toEqual),
AutoDiscovery.findClientConfig("example.org").then((config) => expect(config).toEqual(expected)),
]);
});
+2 -2
View File
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
describe("makeBeaconInfoContent()", () => {
const mockDateNow = 123456789;
beforeEach(() => {
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
vi.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
});
afterAll(() => {
jest.spyOn(globalThis.Date, "now").mockRestore();
vi.spyOn(globalThis.Date, "now").mockRestore();
});
it("create fully defined event content", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
+4 -1
View File
@@ -14,8 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @vitest-environment happy-dom
*/
import "fake-indexeddb/auto";
import "jest-localstorage-mock";
import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src";
import { type CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
+2 -7
View File
@@ -29,12 +29,7 @@ describe("sha256", () => {
});
it("throws if webcrypto is not available", async () => {
const oldCrypto = globalThis.crypto;
try {
globalThis.crypto = {} as any;
await expect(sha256("test")).rejects.toThrow();
} finally {
globalThis.crypto = oldCrypto;
}
vi.stubGlobal("crypto", {});
await expect(sha256("test")).rejects.toThrow();
});
});
+220 -44
View File
@@ -1,7 +1,3 @@
/**
* @jest-environment jsdom
*/
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
@@ -22,7 +18,7 @@ limitations under the License.
// project, which doesn't know about our TypeEventEmitter implementation at all
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { type MockedObject } from "jest-mock";
import { type MockedObject } from "vitest";
import {
type WidgetApi,
WidgetApiToWidgetAction,
@@ -36,7 +32,14 @@ import {
type IRoomEvent,
} from "matrix-widget-api";
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import {
createRoomWidgetClient,
EventType,
type IEvent,
MatrixError,
MsgType,
UpdateDelayedEventAction,
} from "../../src/matrix";
import { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
@@ -46,6 +49,7 @@ import { sleep } from "../../src/utils";
import { SlidingSync } from "../../src/sliding-sync";
import { logger } from "../../src/logger";
import { flushPromises } from "../test-utils/flushPromises";
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
const testOIDCToken = {
access_token: "12345678",
@@ -53,27 +57,28 @@ const testOIDCToken = {
matrix_server_name: "homeserver.oabc",
token_type: "Bearer",
};
class MockWidgetApi extends EventEmitter {
public start = jest.fn().mockResolvedValue(undefined);
public getClientVersions = jest.fn();
public requestCapability = jest.fn().mockResolvedValue(undefined);
public requestCapabilities = jest.fn().mockResolvedValue(undefined);
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendEvent = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveEvent = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendMessage = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveMessage = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendState = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveState = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendToDevice = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveToDevice = jest.fn().mockResolvedValue(undefined);
public sendRoomEvent = jest.fn(
public start = vi.fn().mockResolvedValue(undefined);
public getClientVersions = vi.fn();
public requestCapability = vi.fn().mockResolvedValue(undefined);
public requestCapabilities = vi.fn().mockResolvedValue(undefined);
public requestCapabilityForRoomTimeline = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendEvent = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveEvent = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendMessage = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveMessage = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendState = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveState = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendToDevice = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveToDevice = vi.fn().mockResolvedValue(undefined);
public sendRoomEvent = vi.fn(
async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public sendStateEvent = jest.fn(
public sendStateEvent = vi.fn(
async (
eventType: string,
stateKey: string,
@@ -86,24 +91,24 @@ class MockWidgetApi extends EventEmitter {
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public cancelScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
public restartScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
public sendScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
public sendToDevice = jest.fn().mockResolvedValue(undefined);
public requestOpenIDConnectToken = jest.fn(async () => {
public cancelScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
public restartScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
public sendScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
public sendToDevice = vi.fn().mockResolvedValue(undefined);
public requestOpenIDConnectToken = vi.fn(async () => {
return testOIDCToken;
return new Promise<IOpenIDCredentials>(() => {
return testOIDCToken;
});
});
public readStateEvents = jest.fn(async () => []);
public getTurnServers = jest.fn(async () => []);
public sendContentLoaded = jest.fn().mockResolvedValue(undefined);
public readStateEvents = vi.fn(async () => []);
public getTurnServers = vi.fn(async () => []);
public sendContentLoaded = vi.fn().mockResolvedValue(undefined);
public transport = {
reply: jest.fn(),
send: jest.fn(),
sendComplete: jest.fn(),
reply: vi.fn(),
send: vi.fn(),
sendComplete: vi.fn(),
};
/**
@@ -171,6 +176,9 @@ describe("RoomWidgetClient", () => {
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
undefined,
undefined,
undefined,
);
});
@@ -230,7 +238,7 @@ describe("RoomWidgetClient", () => {
);
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
const injectSpy = jest.spyOn((client as any).syncApi, "injectRoomEvents");
const injectSpy = vi.spyOn((client as any).syncApi, "injectRoomEvents");
const widgetSendEmitter = new EventEmitter();
const widgetSendPromise = new Promise<void>((resolve) =>
widgetSendEmitter.once("send", () => resolve()),
@@ -357,10 +365,10 @@ describe("RoomWidgetClient", () => {
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to send");
widgetApi.transport.send.mockRejectedValue(error);
vi.mocked(widgetApi.transport.send).mockRejectedValue(error);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
@@ -383,22 +391,22 @@ describe("RoomWidgetClient", () => {
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl, undefined, expect.any(Headers));
widgetApi.transport.send.mockRejectedValue(widgetError);
vi.mocked(widgetApi.transport.send).mockRejectedValue(widgetError);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow(matrixError);
).rejects.toStrictEqual(matrixError);
});
});
describe("delayed events", () => {
describe("when supported", () => {
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"),
);
@@ -426,6 +434,7 @@ describe("RoomWidgetClient", () => {
"!1:example.org",
2000,
undefined,
undefined,
);
});
@@ -446,6 +455,7 @@ describe("RoomWidgetClient", () => {
"!1:example.org",
undefined,
parentDelayId,
undefined,
);
});
@@ -580,6 +590,16 @@ describe("RoomWidgetClient", () => {
});
describe("when unsupported", () => {
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
beforeAll(() => {
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
});
afterAll(() => {
doesServerSupportUnstableFeatureMock.mockReset();
});
it("fails to send delayed message events", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
await expect(
@@ -770,7 +790,7 @@ describe("RoomWidgetClient", () => {
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
const logSpy = jest.spyOn(logger, "error");
const logSpy = vi.spyOn(logger, "error");
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
@@ -849,6 +869,162 @@ describe("RoomWidgetClient", () => {
});
});
describe("sticky events", () => {
describe("when supported", () => {
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4354"),
);
beforeAll(() => {
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
});
afterAll(() => {
doesServerSupportUnstableFeatureMock.mockReset();
});
it("requests capabilities when set", async () => {
await makeClient({ sendSticky: true, receiveSticky: true });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407ReceiveStickyEvent);
});
it("does not request capabilities when unset", async () => {
await makeClient({});
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(
MatrixCapabilities.MSC4407ReceiveStickyEvent,
);
});
it("sends", async () => {
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith(EventType.RTCMembership);
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
await client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
msc4354_sticky_key: "test",
});
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
EventType.RTCMembership,
{ msc4354_sticky_key: "test" },
"!1:example.org",
undefined,
undefined,
2000,
);
});
it("receives (adds, updates, then removes when redacted)", async () => {
await makeClient({ receiveEvent: [EventType.RTCMembership, EventType.RoomRedaction] });
const room = client.getRoom("!1:example.org")!;
function expectStickyEvents(events: IEvent[]) {
expect([...room._unstable_getStickyEvents()].map((e) => e.getEffectiveEvent())).toEqual(events);
}
async function sendAndExpectStickyUpdate(
eventToSend: IEvent,
added: IEvent[],
updated: { current: IEvent; previous: IEvent }[],
removed: IEvent[],
) {
const emittedStickyUpdate = new Promise<
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
>((resolve) => room.once(RoomStickyEventsEvent.Update, (...args) => resolve(args)));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
detail: { data: eventToSend },
}),
);
const [addedReceived, updatedReceived, removedReceived] = await emittedStickyUpdate;
expect(addedReceived.map((e) => e.getEffectiveEvent())).toEqual(added);
expect(
updatedReceived.map(({ current, previous }) => ({
current: current.getEffectiveEvent(),
previous: previous.getEffectiveEvent(),
})),
).toEqual(updated);
expect(removedReceived.map((e) => e.getEffectiveEvent())).toEqual(removed);
}
// First, add a new sticky event to the map. The client should emit.
const event1 = new MatrixEvent({
type: EventType.RTCMembership,
event_id: "$pduhfiidph",
room_id: "!1:example.org",
sender: "@alice:example.org",
msc4354_sticky: { duration_ms: 1200000 },
content: { msc4354_sticky_key: "test" },
}).getEffectiveEvent();
await sendAndExpectStickyUpdate(event1, [event1], [], []);
// It should remain cached in the sticky map
expectStickyEvents([event1]);
// Next, update the same key in the sticky map
const event2 = new MatrixEvent({
type: EventType.RTCMembership,
event_id: "$zshgyutptfh",
room_id: "!1:example.org",
sender: "@alice:example.org",
msc4354_sticky: { duration_ms: 1200000 },
content: { msc4354_sticky_key: "test" },
}).getEffectiveEvent();
await sendAndExpectStickyUpdate(event2, [], [{ current: event2, previous: event1 }], []);
expectStickyEvents([event2]);
// Next, redact the second event. Because it has the first as a predecessor, the map should revert to
// the first event.
const redaction1 = new MatrixEvent({
type: EventType.RoomRedaction,
event_id: "$cimoexnvz",
room_id: "!1:example.org",
sender: "@alice:example.org",
redacts: event2.event_id,
content: { redacts: event2.event_id },
}).getEffectiveEvent();
await sendAndExpectStickyUpdate(redaction1, [], [{ current: event1, previous: event2 }], []);
expectStickyEvents([event1]);
// Finally, redact the first event. Now everything should be gone from the map.
const redaction2 = new MatrixEvent({
type: EventType.RoomRedaction,
event_id: "$drgzmenlh",
room_id: "!1:example.org",
sender: "@alice:example.org",
redacts: event1.event_id,
content: { redacts: event1.event_id },
}).getEffectiveEvent();
await sendAndExpectStickyUpdate(redaction2, [], [], [event1]);
expectStickyEvents([]);
});
});
describe("when unsupported", () => {
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
beforeAll(() => {
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
});
afterAll(() => {
doesServerSupportUnstableFeatureMock.mockReset();
});
it("fails to send", async () => {
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
await expect(
client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
msc4354_sticky_key: "test",
}),
).rejects.toThrow("Server does not support");
});
});
});
describe("to-device messages", () => {
const unencryptedContentMap = new Map([
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
@@ -970,7 +1146,7 @@ describe("RoomWidgetClient", () => {
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to get token");
widgetApi.transport.sendComplete.mockRejectedValue(error);
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(error);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
@@ -994,9 +1170,9 @@ describe("RoomWidgetClient", () => {
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl, undefined, expect.any(Headers));
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(widgetError);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
+3 -3
View File
@@ -37,7 +37,7 @@ describe("eventMapperFor", function () {
setUserCreator(_) {},
} as IStore,
scheduler: {
setProcessFunction: jest.fn(),
setProcessFunction: vi.fn(),
} as unknown as MatrixScheduler,
userId: userId,
});
@@ -133,7 +133,7 @@ describe("eventMapperFor", function () {
event_id: eventId,
};
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
const decryptEventIfNeededSpy = vi.spyOn(client, "decryptEventIfNeeded");
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
const mapper = eventMapperFor(client, {
@@ -161,7 +161,7 @@ describe("eventMapperFor", function () {
event_id: eventId,
};
const evListener = jest.fn();
const evListener = vi.fn();
client.on(MatrixEventEvent.Replaced, evListener);
const noReEmitMapper = eventMapperFor(client, {
+12 -12
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { type MockInstance } from "vitest";
import * as utils from "../test-utils/test-utils";
import {
@@ -196,7 +196,7 @@ describe("EventTimelineSet", () => {
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);
vi.mocked(client.supportsThreads).mockReturnValue(true);
const reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
const liveTimeline = eventTimelineSet.getLiveTimeline();
@@ -228,7 +228,7 @@ describe("EventTimelineSet", () => {
let thread: Thread;
beforeEach(() => {
(client.supportsThreads as jest.Mock).mockReturnValue(true);
vi.mocked(client.supportsThreads).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
@@ -306,20 +306,20 @@ describe("EventTimelineSet", () => {
});
describe("with events to be decrypted", () => {
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
let messageEventShouldAttemptDecryptionSpy: MockInstance;
let messageEventIsDecryptionFailureSpy: MockInstance;
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance;
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
let replyEventShouldAttemptDecryptionSpy: MockInstance;
let replyEventIsDecryptionFailureSpy: MockInstance;
beforeEach(() => {
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption");
messageEventShouldAttemptDecryptionSpy = vi.spyOn(messageEvent, "shouldAttemptDecryption");
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
messageEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, "shouldAttemptDecryption");
replyEventShouldAttemptDecryptionSpy = vi.spyOn(replyEvent, "shouldAttemptDecryption");
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
replyEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
});
@@ -384,7 +384,7 @@ describe("EventTimelineSet", () => {
let thread: Thread;
beforeEach(() => {
(client.supportsThreads as jest.Mock).mockReturnValue(true);
vi.mocked(client.supportsThreads).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
+12 -14
View File
@@ -1,5 +1,3 @@
import { mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils";
import { Direction, EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
@@ -20,21 +18,21 @@ describe("EventTimeline", function () {
const getTimeline = (): EventTimeline => {
const room = new Room(roomId, mockClient, userA);
const timelineSet = new EventTimelineSet(room);
jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
vi.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
const timeline = new EventTimeline(timelineSet);
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
// otherwise the default member property values (e.g. paginationToken) will be incorrect
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
timeline.getState(Direction.Backward)!.setStateEvents = vi.fn();
timeline.getState(Direction.Backward)!.getSentinelMember = vi.fn();
timeline.getState(Direction.Forward)!.setStateEvents = vi.fn();
timeline.getState(Direction.Forward)!.getSentinelMember = vi.fn();
return timeline;
};
beforeEach(function () {
// reset any RoomState mocks
jest.resetAllMocks();
vi.resetAllMocks();
timeline = getTimeline();
});
@@ -67,12 +65,12 @@ describe("EventTimeline", function () {
timeline.initialiseState(events);
// @ts-ignore private prop
const timelineStartState = timeline.startState!;
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
expect(vi.mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
timelineWasEmpty: undefined,
});
// @ts-ignore private prop
const timelineEndState = timeline.endState!;
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
expect(vi.mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
timelineWasEmpty: undefined,
});
});
@@ -210,13 +208,13 @@ describe("EventTimeline", function () {
sentinel.name = "Old Alice";
sentinel.membership = KnownMembership.Join;
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
return oldSentinel;
}
@@ -253,13 +251,13 @@ describe("EventTimeline", function () {
sentinel.name = "Old Alice";
sentinel.membership = KnownMembership.Join;
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
return oldSentinel;
}
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
exports[`MatrixHttpApi > should return expected object from \`getContentUri\` 1`] = `
{
"base": "http://baseUrl",
"params": {
+1 -1
View File
@@ -57,7 +57,7 @@ describe("MatrixError", () => {
it("should retrieve Date Retry-After header from rate-limit error", () => {
headers.set("Retry-After", `${new Date(160000).toUTCString()}`);
jest.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
vi.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
// prefer Retry-After header over retry_after_ms
+85 -77
View File
@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { Mocked, MockedFunction } from "jest-mock";
import { type Mocked, type MockedFunction } from "vitest";
import { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import {
@@ -26,6 +27,7 @@ import {
type IHttpOpts,
MatrixError,
Method,
TokenRefreshError,
} from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
import { type QueryDict, sleep } from "../../../src/utils";
@@ -38,7 +40,7 @@ describe("FetchHttpApi", () => {
const tokenInactiveError = new MatrixError({ errcode: "M_UNKNOWN_TOKEN", error: "Token is not active" }, 401);
beforeEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});
it("should support aborting multiple times", () => {
@@ -47,29 +49,29 @@ describe("FetchHttpApi", () => {
api.request(Method.Get, "/foo");
api.request(Method.Get, "/baz");
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
expect((fetchFn.mock.calls[0][0] as URL).href.endsWith("/foo")).toBeTruthy();
expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeFalsy();
expect((fetchFn.mock.calls[1][0] as URL).href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeTruthy();
api.request(Method.Get, "/bar");
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
expect((fetchFn.mock.calls[2][0] as URL).href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeTruthy();
});
it("should fall back to global fetch if fetchFn not provided", () => {
globalThis.fetch = jest.fn();
expect(globalThis.fetch).not.toHaveBeenCalled();
const spy = (globalThis.fetch = vi.fn());
expect(spy).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
api.fetch("test");
expect(globalThis.fetch).toHaveBeenCalled();
expect(spy).toHaveBeenCalled();
});
it("should update identity server base url", () => {
@@ -97,8 +99,8 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).toBe("bar");
expect((fetchFn.mock.calls[0][0] as URL).searchParams.getAll("via")).toEqual(["a", "b"]);
});
it("should send params as body for non-GET requests", () => {
@@ -112,8 +114,8 @@ describe("FetchHttpApi", () => {
});
const params = { foo: "bar", via: ["a", "b"] };
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1]!.body as string)).toStrictEqual(params);
});
it("should add Authorization header if token provided", () => {
@@ -126,7 +128,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>).Authorization).toBe("Bearer token");
});
});
@@ -142,7 +144,7 @@ describe("FetchHttpApi", () => {
it("should set an Accept header, and parse the response as JSON, by default", async () => {
const result = { a: 1 };
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue(result) });
const fetchFn = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue(result) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(result);
expect(fetchFn.mock.calls[0][1].headers.Accept).toBe("application/json");
@@ -150,7 +152,7 @@ describe("FetchHttpApi", () => {
it("should not set an Accept header, and should return text if json=false", async () => {
const text = "418 I'm a teapot";
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
const fetchFn = vi.fn().mockResolvedValue({ ok: true, text: vi.fn().mockResolvedValue(text) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(
api.requestOtherUrl(Method.Get, "http://url", undefined, {
@@ -162,7 +164,7 @@ describe("FetchHttpApi", () => {
it("should not set an Accept header, and should return a blob, if rawResponseBody is true", async () => {
const blob = new Blob(["blobby"]);
const fetchFn = jest.fn().mockResolvedValue({ ok: true, blob: jest.fn().mockResolvedValue(blob) });
const fetchFn = vi.fn().mockResolvedValue({ ok: true, blob: vi.fn().mockResolvedValue(blob) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(
api.requestOtherUrl(Method.Get, "http://url", undefined, {
@@ -176,7 +178,7 @@ describe("FetchHttpApi", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn: jest.fn(),
fetchFn: vi.fn(),
onlyData: true,
});
await expect(
@@ -195,7 +197,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
await api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBe("token");
});
it("should send token via headers by default", async () => {
@@ -208,7 +210,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
await api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer token");
});
it("should not send a token if not calling `authedRequest`", () => {
@@ -221,8 +223,8 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
api.request(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBeFalsy();
});
it("should ensure no token is leaked out via query params if sending via headers", async () => {
@@ -236,8 +238,8 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
await api.authedRequest(Method.Get, "/path", { access_token: "123" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer token");
});
it("should not override manually specified access token via query params", async () => {
@@ -251,7 +253,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
await api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBe("RealToken");
});
it("should not override manually specified access token via header", async () => {
@@ -267,7 +269,7 @@ describe("FetchHttpApi", () => {
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Authorization: "Bearer RealToken" },
});
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer RealToken");
});
it("should not override Accept header", async () => {
@@ -276,18 +278,18 @@ describe("FetchHttpApi", () => {
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Accept: "text/html" },
});
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Accept"]).toBe("text/html");
});
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
const fetchFn = jest.fn().mockResolvedValue({
const fetchFn = vi.fn().mockResolvedValue({
ok: false,
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(
text: vi.fn().mockResolvedValue(
JSON.stringify({
errcode: "M_CONSENT_NOT_GIVEN",
error: "Ye shall ask for consent",
@@ -309,7 +311,7 @@ describe("FetchHttpApi", () => {
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, onlyData: true });
await api.authedRequest(Method.Post, "/account/password");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>).Authorization).toBeUndefined();
});
describe("with refresh token", () => {
@@ -322,7 +324,13 @@ describe("FetchHttpApi", () => {
error: "Token is not active",
soft_logout: false,
};
const unknownTokenErr = new MatrixError(unknownTokenErrBody, 401);
const unknownTokenErr = new MatrixError(
unknownTokenErrBody,
401,
undefined,
undefined,
expect.anything(),
);
const unknownTokenResponse = {
ok: false,
status: 401,
@@ -331,19 +339,19 @@ describe("FetchHttpApi", () => {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
text: vi.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
};
const okayResponse = {
ok: true,
status: 200,
json: jest.fn().mockResolvedValue({ x: 1 }),
json: vi.fn().mockResolvedValue({ x: 1 }),
};
describe("without a tokenRefreshFunction", () => {
it("should emit logout and throw", async () => {
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -362,10 +370,10 @@ describe("FetchHttpApi", () => {
describe("with a tokenRefreshFunction", () => {
it("should emit logout and throw when token refresh fails", async () => {
const error = new MatrixError();
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -375,7 +383,7 @@ describe("FetchHttpApi", () => {
refreshToken,
onlyData: true,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toEqual(
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
unknownTokenErr,
);
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
@@ -384,10 +392,10 @@ describe("FetchHttpApi", () => {
it("should not emit logout but still throw when token refresh fails due to transitive fault", async () => {
const error = new ConnectionError("transitive fault");
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -397,8 +405,8 @@ describe("FetchHttpApi", () => {
refreshToken,
onlyData: true,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toEqual(
unknownTokenErr,
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
new TokenRefreshError(unknownTokenErr),
);
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
@@ -407,16 +415,16 @@ describe("FetchHttpApi", () => {
it("should refresh token and retry request", async () => {
const newAccessToken = "new-access-token";
const newRefreshToken = "new-refresh-token";
const tokenRefreshFunction = jest.fn().mockResolvedValue({
const tokenRefreshFunction = vi.fn().mockResolvedValue({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
});
const fetchFn = jest
const fetchFn = vi
.fn()
.mockResolvedValueOnce(unknownTokenResponse)
.mockResolvedValueOnce(okayResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -448,7 +456,7 @@ describe("FetchHttpApi", () => {
// count because it's only to get a token with an expiry)
const newAccessToken = "new-access-token";
const newRefreshToken = "new-refresh-token";
const tokenRefreshFunction = jest.fn().mockReturnValue({
const tokenRefreshFunction = vi.fn().mockReturnValue({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
// This needs to be sufficiently high that it's over the threshold for
@@ -457,10 +465,10 @@ describe("FetchHttpApi", () => {
});
// fetch doesn't like our new or old tokens
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -470,7 +478,7 @@ describe("FetchHttpApi", () => {
refreshToken,
onlyData: true,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
unknownTokenErr,
);
@@ -492,7 +500,7 @@ describe("FetchHttpApi", () => {
// first refresh is to get a token with an expiry at all, because we
// can't specify an expiry on the token we inject
const tokenRefreshFunction = jest.fn().mockResolvedValueOnce({
const tokenRefreshFunction = vi.fn().mockResolvedValueOnce({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiry: new Date(Date.now() + 1000),
@@ -513,10 +521,10 @@ describe("FetchHttpApi", () => {
expiry: new Date(Date.now() + 5 * 60 * 1000),
});
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -526,7 +534,7 @@ describe("FetchHttpApi", () => {
refreshToken,
onlyData: true,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
unknownTokenErr,
);
@@ -543,7 +551,7 @@ describe("FetchHttpApi", () => {
const localBaseUrl = "http://baseurl";
const baseUrlWithTrailingSlash = "http://baseurl/";
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
const fetchFn = jest.fn();
const fetchFn = vi.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn, onlyData: true });
};
@@ -596,7 +604,7 @@ describe("FetchHttpApi", () => {
describe("extraParams handling", () => {
const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => {
const fetchFn = jest.fn();
const fetchFn = vi.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
return new FetchHttpApi(emitter, {
baseUrl: localBaseUrl,
@@ -655,7 +663,7 @@ describe("FetchHttpApi", () => {
});
it("should work when extraParams is undefined", () => {
const fetchFn = jest.fn();
const fetchFn = vi.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, onlyData: true });
@@ -679,11 +687,11 @@ describe("FetchHttpApi", () => {
});
it("should not log query parameters", async () => {
jest.useFakeTimers();
vi.useFakeTimers();
const responseResolvers = Promise.withResolvers<Response>();
const fetchFn = jest.fn().mockReturnValue(responseResolvers.promise);
const fetchFn = vi.fn().mockReturnValue(responseResolvers.promise);
const mockLogger = {
debug: jest.fn(),
debug: vi.fn(),
} as unknown as Mocked<Logger>;
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
@@ -693,7 +701,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
jest.advanceTimersByTime(1234);
vi.advanceTimersByTime(1234);
responseResolvers.resolve({ ok: true, status: 200, json: () => Promise.resolve("RESPONSE") } as Response);
await prom;
expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
@@ -714,7 +722,7 @@ describe("FetchHttpApi", () => {
it("should not make multiple concurrent refresh token requests", async () => {
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
const fetchFn = jest.fn().mockResolvedValue({
const fetchFn = vi.fn().mockResolvedValue({
ok: false,
status: tokenInactiveError.httpStatus,
async text() {
@@ -724,10 +732,10 @@ describe("FetchHttpApi", () => {
return tokenInactiveError.data;
},
headers: {
get: jest.fn().mockReturnValue("application/json"),
get: vi.fn().mockReturnValue("application/json"),
},
});
const tokenRefreshFunction = jest.fn().mockReturnValue(deferredTokenRefresh.promise);
const tokenRefreshFunction = vi.fn().mockReturnValue(deferredTokenRefresh.promise);
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
@@ -755,7 +763,7 @@ describe("FetchHttpApi", () => {
return {};
},
headers: {
get: jest.fn().mockReturnValue("application/json"),
get: vi.fn().mockReturnValue("application/json"),
},
});
deferredTokenRefresh.resolve({ accessToken: "NEW_ACCESS_TOKEN", refreshToken: "NEW_REFRESH_TOKEN" });
@@ -770,7 +778,7 @@ describe("FetchHttpApi", () => {
it("should use newly refreshed token if request starts mid-refresh", async () => {
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
const fetchFn = jest.fn().mockResolvedValue({
const fetchFn = vi.fn().mockResolvedValue({
ok: false,
status: tokenInactiveError.httpStatus,
async text() {
@@ -780,10 +788,10 @@ describe("FetchHttpApi", () => {
return tokenInactiveError.data;
},
headers: {
get: jest.fn().mockReturnValue("application/json"),
get: vi.fn().mockReturnValue("application/json"),
},
});
const tokenRefreshFunction = jest.fn().mockReturnValue(deferredTokenRefresh.promise);
const tokenRefreshFunction = vi.fn().mockReturnValue(deferredTokenRefresh.promise);
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
@@ -813,7 +821,7 @@ describe("FetchHttpApi", () => {
return {};
},
headers: {
get: jest.fn().mockReturnValue("application/json"),
get: vi.fn().mockReturnValue("application/json"),
},
});
@@ -832,6 +840,6 @@ describe("FetchHttpApi", () => {
});
});
function makeMockFetchFn(): MockedFunction<any> {
return jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
function makeMockFetchFn(): MockedFunction<Window["fetch"]> {
return vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
}
+72 -61
View File
@@ -14,54 +14,59 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { ClientPrefix, MatrixHttpApi, Method, type UploadResponse } from "../../../src";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
jest.useFakeTimers();
vi.useFakeTimers();
describe("MatrixHttpApi", () => {
const baseUrl = "http://baseUrl";
const prefix = ClientPrefix.V3;
let xhr: Writeable<XMLHttpRequest>;
let upload: Promise<UploadResponse>;
const DONE = 0;
function getRequest(): Writeable<XMLHttpRequest> | undefined {
return vi.mocked(globalThis.XMLHttpRequest)?.mock.instances.at(-1);
}
beforeEach(() => {
xhr = {
upload: {} as XMLHttpRequestUpload,
open: jest.fn(),
send: jest.fn(),
abort: jest.fn(),
setRequestHeader: jest.fn(),
onreadystatechange: undefined,
getResponseHeader: jest.fn(),
getAllResponseHeaders: jest.fn(),
} as unknown as XMLHttpRequest;
// We stub out XHR here as it is not available in JSDOM
// We stub out XHR here as it is not available in the test environment
// @ts-ignore
globalThis.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
globalThis.XMLHttpRequest = vi.fn().mockImplementation(function (this: XMLHttpRequest) {
// @ts-ignore
this.upload = {} as XMLHttpRequestUpload;
this.open = vi.fn();
this.send = vi.fn();
this.abort = vi.fn();
this.setRequestHeader = vi.fn();
// @ts-ignore
this.onreadystatechange = undefined;
this.getResponseHeader = vi.fn();
this.getAllResponseHeaders = vi.fn();
});
// @ts-ignore
globalThis.XMLHttpRequest.DONE = DONE;
});
afterEach(() => {
upload?.catch(() => {});
// Abort any remaining requests
xhr.readyState = DONE;
xhr.status = 0;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
const xhr = getRequest();
if (xhr) {
// Abort any remaining requests
xhr.readyState = DONE;
xhr.status = 0;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
}
});
it("should fall back to `fetch` where xhr is unavailable", async () => {
globalThis.XMLHttpRequest = undefined!;
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
const fetchFn = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
upload = api.uploadContent({} as File);
await upload;
@@ -69,11 +74,11 @@ describe("MatrixHttpApi", () => {
});
it("should prefer xhr where available", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const fetchFn = vi.fn().mockResolvedValue({ ok: true });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
upload = api.uploadContent({} as File);
expect(fetchFn).not.toHaveBeenCalled();
expect(xhr.open).toHaveBeenCalled();
expect(getRequest()!.open).toHaveBeenCalled();
});
it("should send access token in query params if header disabled", () => {
@@ -85,11 +90,11 @@ describe("MatrixHttpApi", () => {
onlyData: true,
});
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(
expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token",
);
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
expect(getRequest()!.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
});
it("should send access token in header by default", () => {
@@ -100,14 +105,17 @@ describe("MatrixHttpApi", () => {
onlyData: true,
});
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
);
expect(getRequest()!.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
});
it("should include filename by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File, { name: "name" });
expect(xhr.open).toHaveBeenCalledWith(
expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
);
@@ -116,42 +124,45 @@ describe("MatrixHttpApi", () => {
it("should allow not sending the filename", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
);
});
it("should abort xhr when the upload is aborted", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
api.cancelUpload(upload);
expect(xhr.abort).toHaveBeenCalled();
expect(getRequest()!.abort).toHaveBeenCalled();
return expect(upload).rejects.toThrow("Aborted");
});
it("should timeout if no progress in 30s", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
jest.advanceTimersByTime(25000);
vi.advanceTimersByTime(25000);
// @ts-ignore
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
jest.advanceTimersByTime(25000);
expect(xhr.abort).not.toHaveBeenCalled();
jest.advanceTimersByTime(5000);
expect(xhr.abort).toHaveBeenCalled();
getRequest()!.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
vi.advanceTimersByTime(25000);
expect(getRequest()!.abort).not.toHaveBeenCalled();
vi.advanceTimersByTime(5000);
expect(getRequest()!.abort).toHaveBeenCalled();
});
it("should call progressHandler", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
const progressHandler = jest.fn();
const progressHandler = vi.fn();
upload = api.uploadContent({} as File, { progressHandler });
const progressEvent = new Event("progress") as ProgressEvent;
Object.assign(progressEvent, { loaded: 1, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
getRequest()!.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
Object.assign(progressEvent, { loaded: 95, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
getRequest()!.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
});
@@ -159,11 +170,11 @@ describe("MatrixHttpApi", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = "";
xhr.status = 200;
getRequest()!.readyState = DONE;
getRequest()!.responseText = "";
getRequest()!.status = 200;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
getRequest()!.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("No response body.");
});
@@ -172,15 +183,15 @@ describe("MatrixHttpApi", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
xhr.status = 404;
mocked(xhr.getResponseHeader).mockImplementation((name) =>
getRequest()!.readyState = DONE;
getRequest()!.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
getRequest()!.status = 404;
vi.mocked(getRequest()!.getResponseHeader).mockImplementation((name) =>
name.toLowerCase() === "content-type" ? "application/json" : null,
);
mocked(xhr.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
vi.mocked(getRequest()!.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
getRequest()!.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("Not found");
});
@@ -189,12 +200,12 @@ describe("MatrixHttpApi", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
xhr.status = 200;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
getRequest()!.readyState = DONE;
getRequest()!.responseText = '{"content_uri": "mxc://server/foobar"}';
getRequest()!.status = 200;
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
getRequest()!.onreadystatechange?.(new Event("test"));
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
});
@@ -203,22 +214,22 @@ describe("MatrixHttpApi", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
expect(api.cancelUpload(upload)).toBeTruthy();
expect(xhr.abort).toHaveBeenCalled();
expect(getRequest()!.abort).toHaveBeenCalled();
});
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.status = 500;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
getRequest()!.readyState = DONE;
getRequest()!.status = 500;
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
getRequest()!.onreadystatechange?.(new Event("test"));
await upload.catch(() => {});
expect(api.cancelUpload(upload)).toBeFalsy();
expect(xhr.abort).not.toHaveBeenCalled();
expect(getRequest()!.abort).not.toHaveBeenCalled();
});
it("should return active uploads in `getCurrentUploads`", () => {
+72 -27
View File
@@ -14,51 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import {
anySignal,
ConnectionError,
HTTPError,
MatrixError,
MatrixSafetyError,
MatrixSafetyErrorCode,
parseErrorResponse,
retryNetworkOperation,
timeoutSignal,
} from "../../../src";
import { sleep } from "../../../src/utils";
jest.mock("../../../src/utils");
vi.mock("../../../src/utils");
// setupTests mocks `timeoutSignal` due to hanging timers
jest.unmock("../../../src/http-api/utils");
vi.unmock("../../../src/http-api/utils");
describe("timeoutSignal", () => {
jest.useFakeTimers();
vi.useFakeTimers();
it("should fire abort signal after specified timeout", () => {
const signal = timeoutSignal(3000);
const onabort = jest.fn();
const onabort = vi.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(3000);
vi.advanceTimersByTime(3000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
});
describe("anySignal", () => {
jest.useFakeTimers();
vi.useFakeTimers();
it("should fire when any signal fires", () => {
const { signal } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
const onabort = jest.fn();
const onabort = vi.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(2000);
vi.advanceTimersByTime(2000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
@@ -66,13 +66,13 @@ describe("anySignal", () => {
it("should cleanup when instructed", () => {
const { signal, cleanup } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
const onabort = jest.fn();
const onabort = vi.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
cleanup();
jest.advanceTimersByTime(2000);
vi.advanceTimersByTime(2000);
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
});
@@ -86,9 +86,14 @@ describe("anySignal", () => {
});
describe("parseErrorResponse", () => {
const url = "https://example.org";
let headers: Headers;
const xhrHeaderMethods = {
getResponseHeader: (name: string) => headers.get(name),
responseURL: url,
getResponseHeader: (name: string) => {
headers.get(name);
},
getAllResponseHeaders: () => {
let allHeaders = "";
headers.forEach((value, key) => {
@@ -118,6 +123,9 @@ describe("parseErrorResponse", () => {
errcode: "TEST",
},
500,
url,
undefined,
expect.any(Headers),
),
);
});
@@ -127,6 +135,7 @@ describe("parseErrorResponse", () => {
expect(
parseErrorResponse(
{
url,
headers,
status: 500,
} as Response,
@@ -138,6 +147,9 @@ describe("parseErrorResponse", () => {
errcode: "TEST",
},
500,
url,
undefined,
expect.any(Headers),
),
);
});
@@ -147,8 +159,8 @@ describe("parseErrorResponse", () => {
expect(
parseErrorResponse(
{
responseURL: "https://example.com",
...xhrHeaderMethods,
responseURL: "https://example.com",
status: 500,
} as XMLHttpRequest,
'{"errcode": "TEST"}',
@@ -160,6 +172,8 @@ describe("parseErrorResponse", () => {
},
500,
"https://example.com",
undefined,
expect.any(Headers),
),
);
});
@@ -182,9 +196,40 @@ describe("parseErrorResponse", () => {
},
500,
"https://example.com",
undefined,
expect.any(Headers),
),
);
});
it.each([
{
errcode: MatrixSafetyErrorCode.name,
error: "Spammy",
},
{
errcode: MatrixSafetyErrorCode.name,
error: "Spammy",
expiry: 5000,
},
{
errcode: MatrixSafetyErrorCode.name,
error: "Spammy",
harms: ["m.spam", "org.example.additional-harm"],
expiry: 5000,
},
])("should resolve MatrixSafetyErrors from fetch", (errContent) => {
headers.set("Content-Type", "application/json");
const value = parseErrorResponse(
{
headers,
status: 400,
} as Response,
JSON.stringify(errContent),
) as MatrixSafetyError;
expect(value).toBeInstanceOf(MatrixSafetyError);
expect(value.harms.size).toEqual(errContent.harms?.length ?? 0);
expect(value.expiry?.getTime()).toEqual(errContent.expiry);
});
describe("with HTTP headers", () => {
function addHeaders(headers: Headers) {
@@ -196,7 +241,7 @@ describe("parseErrorResponse", () => {
}
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) {
expect(new Map(otherHeaders)).toEqual(new Map(expectedHeaders));
expect(new Map(otherHeaders as any)).toEqual(new Map(expectedHeaders as any));
}
it("should resolve HTTP Errors from XHR with headers", () => {
@@ -265,7 +310,7 @@ describe("parseErrorResponse", () => {
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(new HTTPError("Server returned 500 error", 500));
).toStrictEqual(new HTTPError("Server returned 500 error", 500, expect.any(Headers)));
});
it("should handle empty type gracefully", () => {
@@ -304,27 +349,27 @@ describe("parseErrorResponse", () => {
} as Response,
"I'm a teapot",
),
).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418));
).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418, expect.any(Headers)));
});
});
describe("retryNetworkOperation", () => {
it("should retry given number of times with exponential sleeps", async () => {
const err = new ConnectionError("test");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
const fn = vi.fn().mockRejectedValue(err);
vi.mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(4);
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
expect(vi.mocked(sleep)).toHaveBeenCalledTimes(3);
expect(vi.mocked(sleep).mock.calls[0][0]).toBe(2000);
expect(vi.mocked(sleep).mock.calls[1][0]).toBe(4000);
expect(vi.mocked(sleep).mock.calls[2][0]).toBe(8000);
});
it("should bail out on errors other than ConnectionError", async () => {
const err = new TypeError("invalid JSON");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
const fn = vi.fn().mockRejectedValue(err);
vi.mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(1);
});
@@ -334,10 +379,10 @@ describe("retryNetworkOperation", () => {
const err2 = new ConnectionError("test2");
const err3 = new ConnectionError("test3");
const errors = [err1, err2, err3];
const fn = jest.fn().mockImplementation(() => {
const fn = vi.fn().mockImplementation(() => {
throw errors.shift();
});
mocked(sleep).mockResolvedValue(undefined);
vi.mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
});
});
+56 -52
View File
@@ -17,7 +17,7 @@ limitations under the License.
import { type MatrixClient } from "../../src/client";
import { logger } from "../../src/logger";
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
import { InteractiveAuth, AuthType, NoAuthFlowFoundError } from "../../src/interactive-auth";
import { HTTPError, MatrixError } from "../../src/http-api";
import { sleep } from "../../src/utils";
import { secureRandomString } from "../../src/randomstring";
@@ -34,14 +34,14 @@ const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixC
describe("InteractiveAuth", () => {
it("should start an auth stage and complete it", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Password] }],
@@ -83,14 +83,14 @@ describe("InteractiveAuth", () => {
});
it("should handle auth errcode presence", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Password] }],
@@ -132,9 +132,9 @@ describe("InteractiveAuth", () => {
});
it("should handle set emailSid for email flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
doRequest,
@@ -186,9 +186,9 @@ describe("InteractiveAuth", () => {
});
it("should make a request if no authdata is provided", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -248,9 +248,9 @@ describe("InteractiveAuth", () => {
});
it("should make a request if authdata is null", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -310,9 +310,9 @@ describe("InteractiveAuth", () => {
});
it("should start an auth stage and reject if no auth flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -337,13 +337,15 @@ describe("InteractiveAuth", () => {
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new NoAuthFlowFoundError("No appropriate authentication flow found", [], []),
);
});
it("should start an auth stage and reject if no auth flow but has session", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -372,13 +374,15 @@ describe("InteractiveAuth", () => {
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new NoAuthFlowFoundError("No appropriate authentication flow found", [], []),
);
});
it("should handle unexpected error types without data property set", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -397,13 +401,13 @@ describe("InteractiveAuth", () => {
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("myerror"));
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new HTTPError("myerror", 401));
});
it("should allow dummy auth", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -435,9 +439,9 @@ describe("InteractiveAuth", () => {
describe("requestEmailToken", () => {
it("increases auth attempts", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
const ia = new InteractiveAuth({
@@ -464,9 +468,9 @@ describe("InteractiveAuth", () => {
});
it("passes errors through", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
requestEmailToken.mockImplementation(async () => {
throw new Error("unspecific network error");
});
@@ -482,9 +486,9 @@ describe("InteractiveAuth", () => {
});
it("only starts one request at a time", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
const ia = new InteractiveAuth({
@@ -499,9 +503,9 @@ describe("InteractiveAuth", () => {
});
it("stores result in email sid", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const sid = secureRandomString(24);
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
@@ -518,14 +522,14 @@ describe("InteractiveAuth", () => {
});
it("should prioritise shorter flows", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
@@ -539,14 +543,14 @@ describe("InteractiveAuth", () => {
});
it("should prioritise flows with entirely supported stages", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }],
@@ -561,14 +565,14 @@ describe("InteractiveAuth", () => {
});
it("should fire stateUpdated callback with error when a request fails", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Password] }],
+1 -1
View File
@@ -23,7 +23,7 @@ let client: MatrixClient;
describe("Local notification settings", () => {
beforeEach(() => {
client = new TestClient("@alice:matrix.org", "123", undefined, undefined, undefined).client;
client.setAccountData = jest.fn();
client.setAccountData = vi.fn();
});
describe("Lets you set local notification settings", () => {
+7 -7
View File
@@ -21,12 +21,12 @@ import loglevel from "loglevel";
import { DebugLogger, logger } from "../../src/logger.ts";
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
describe("logger", () => {
it("should log to console by default", () => {
jest.spyOn(console, "debug").mockReturnValue(undefined);
vi.spyOn(console, "debug").mockReturnValue(undefined);
logger.debug("test1");
logger.log("test2");
@@ -35,8 +35,8 @@ describe("logger", () => {
});
it("should allow creation of child loggers which add a prefix", () => {
jest.spyOn(loglevel, "getLogger");
jest.spyOn(console, "debug").mockReturnValue(undefined);
vi.spyOn(loglevel, "getLogger");
vi.spyOn(console, "debug").mockReturnValue(undefined);
const childLogger = logger.getChild("[prefix1]");
expect(loglevel.getLogger).toHaveBeenCalledWith("matrix-[prefix1]");
@@ -52,7 +52,7 @@ describe("logger", () => {
describe("DebugLogger", () => {
it("should handle empty log messages", () => {
const mockTarget = jest.fn();
const mockTarget = vi.fn();
const logger = new DebugLogger(mockTarget as any);
logger.info();
expect(mockTarget).toHaveBeenCalledTimes(1);
@@ -60,7 +60,7 @@ describe("DebugLogger", () => {
});
it("should handle logging an Error", () => {
const mockTarget = jest.fn();
const mockTarget = vi.fn();
const logger = new DebugLogger(mockTarget as any);
// If there is a stack and a message, we use the stack.
@@ -79,7 +79,7 @@ describe("DebugLogger", () => {
});
it("should handle logging an object", () => {
const mockTarget = jest.fn();
const mockTarget = vi.fn();
const logger = new DebugLogger(mockTarget as any);
const obj = { a: 1 };
+4 -5
View File
@@ -1,4 +1,4 @@
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { ClientPrefix, MatrixClient } from "../../src";
import { SSOAction } from "../../src/@types/auth";
@@ -51,27 +51,26 @@ describe("SSO login URL", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
const url = new URL(urlString);
expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
expect(url.searchParams.has("action")).toBe(false);
});
it("register", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
const url = new URL(urlString);
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
expect(url.searchParams.get("action")).toEqual("register");
});
it("login", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
const url = new URL(urlString);
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
expect(url.searchParams.get("action")).toEqual("login");
});
});
});
describe("refreshToken", () => {
afterEach(() => {
fetchMock.mockReset();
});
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
const client = createExampleMatrixClient();
File diff suppressed because it is too large Load Diff
+128 -85
View File
@@ -1,5 +1,5 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Copyright 2023-2026 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.
@@ -14,31 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MatrixEvent } from "../../../src";
import {
CallMembership,
type SessionMembershipData,
DEFAULT_EXPIRE_DURATION,
type RtcMembershipData,
} from "../../../src/matrixrtc/CallMembership";
import { membershipTemplate } from "./mocks";
import { type RtcMembershipData, type SessionMembershipData } from "../../../src/matrixrtc/membershipData/index.ts";
import { type IContent, type MatrixEvent } from "../../../src/models/event.ts";
import { EventType } from "../../../src/@types/event.ts";
import { CallMembership, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership.ts";
function makeMockEvent(originTs = 0): MatrixEvent {
return {
getTs: jest.fn().mockReturnValue(originTs),
getSender: jest.fn().mockReturnValue("@alice:example.org"),
getId: jest.fn().mockReturnValue("$eventid"),
} as unknown as MatrixEvent;
function createCallMembership(ev: MatrixEvent, content: IContent): CallMembership {
vi.mocked(ev.getContent).mockReturnValue(content);
const data = CallMembership.membershipDataFromMatrixEvent(ev);
return new CallMembership(ev, data, "xx");
}
describe("CallMembership", () => {
describe("SessionMembershipData", () => {
function makeMockEvent(originTs = 0): MatrixEvent {
return {
getTs: vi.fn().mockReturnValue(originTs),
getSender: vi.fn().mockReturnValue("@alice:example.org"),
getId: vi.fn().mockReturnValue("$eventid"),
getContent: vi.fn().mockReturnValue({}),
getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
} as unknown as MatrixEvent;
}
beforeEach(() => {
jest.useFakeTimers();
vi.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});
const membershipTemplate: SessionMembershipData = {
@@ -53,29 +56,29 @@ describe("CallMembership", () => {
it("rejects membership with no device_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
createCallMembership(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 }));
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
}).toThrow();
});
it("allow membership with no scope", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
}).not.toThrow();
});
it("uses event timestamp if no created_ts", () => {
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
const membership = createCallMembership(makeMockEvent(12345), membershipTemplate);
expect(membership.createdTs()).toEqual(12345);
});
it("uses created_ts if present", () => {
const membership = new CallMembership(
const membership = createCallMembership(
makeMockEvent(12345),
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
);
@@ -84,28 +87,28 @@ describe("CallMembership", () => {
it("considers memberships unexpired if local age low enough", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
});
it("considers memberships expired if local age large enough", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
});
it("returns preferred foci", () => {
const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" };
const membership = new CallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
const membership = createCallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
expect(membership.transports).toEqual([mockFocus]);
});
describe("getTransport", () => {
const mockFocus = { type: "this_is_a_mock_focus" };
const oldestMembership = new CallMembership(makeMockEvent(), membershipTemplate);
const oldestMembership = createCallMembership(makeMockEvent(), membershipTemplate);
it("gets the correct active transport with oldest_membership", () => {
const membership = new CallMembership(makeMockEvent(), {
const membership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
@@ -119,7 +122,7 @@ describe("CallMembership", () => {
});
it("gets the correct active transport with multi_sfu", () => {
const membership = new CallMembership(makeMockEvent(), {
const membership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "multi_sfu" },
@@ -132,7 +135,7 @@ describe("CallMembership", () => {
expect(membership.getTransport(oldestMembership)).toBe(mockFocus);
});
it("does not provide focus if the selection method is unknown", () => {
const membership = new CallMembership(makeMockEvent(), {
const membership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "unknown" },
@@ -143,7 +146,7 @@ describe("CallMembership", () => {
});
});
describe("correct values from computed fields", () => {
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org");
});
@@ -151,8 +154,35 @@ describe("CallMembership", () => {
expect(membership.eventId).toBe("$eventid");
});
it("returns correct slot_id", () => {
expect(membership.slotId).toBe("m.call#");
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
// slot_id is application and call_id dependent. So we create
// a membership for each possible combination
// non call application (should not alter call_id even with empty string)
const nonCallMembership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
application: "m.not.a.call",
call_id: "",
});
// non "" call id should not be altered
const callMembershipCustomId = createCallMembership(makeMockEvent(), {
...membershipTemplate,
call_id: "customCallId",
});
// for membership (application = m.call and call_id = "") we expect "" -> ROOM
// for legacy events we expect the room to be added automagically
// See INFO_SLOT_ID_LEGACY_CASE comments
expect(membership.slotId).toBe("m.call#ROOM");
expect(membership.slotDescription).toStrictEqual({ id: "ROOM", application: "m.call" });
expect(nonCallMembership.slotId).toBe("m.not.a.call#");
expect(nonCallMembership.slotDescription).toStrictEqual({ id: "", application: "m.not.a.call" });
expect(callMembershipCustomId.slotId).toBe("m.call#customCallId");
expect(callMembershipCustomId.slotDescription).toStrictEqual({
id: "customCallId",
application: "m.call",
});
});
it("returns correct deviceId", () => {
expect(membership.deviceId).toBe("AAAAAAA");
@@ -170,7 +200,7 @@ describe("CallMembership", () => {
expect(membership.scope).toBe("m.room");
});
it("returns correct membershipID", () => {
expect(membership.membershipID).toBe("0");
expect(membership.membershipID).toBe("@alice:example.org:AAAAAAA");
});
it("returns correct unused fields", () => {
expect(membership.getAbsoluteExpiry()).toBe(DEFAULT_EXPIRE_DURATION);
@@ -178,9 +208,40 @@ describe("CallMembership", () => {
expect(membership.isExpired()).toBe(true);
});
});
describe("expiry calculation", () => {
let fakeEvent: MatrixEvent;
let membership: CallMembership;
beforeEach(() => {
// server origin timestamp for this event is 1000
fakeEvent = makeMockEvent(1000);
membership = createCallMembership(fakeEvent!, membershipTemplate);
vi.useFakeTimers();
});
afterEach(() => {
vi.useFakeTimers();
});
it("calculates time until expiry", () => {
vi.setSystemTime(2000);
// should be using absolute expiry time
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
});
});
});
describe("RtcMembershipData", () => {
function makeMockEvent(originTs = 0, content: IContent = {}): MatrixEvent {
return {
getTs: vi.fn().mockReturnValue(originTs),
getSender: vi.fn().mockReturnValue("@alice:example.org"),
getId: vi.fn().mockReturnValue("$eventid"),
getContent: vi.fn().mockReturnValue(content),
getType: vi.fn().mockReturnValue(EventType.RTCMembership),
} as unknown as MatrixEvent;
}
const membershipTemplate: RtcMembershipData = {
slot_id: "m.call#",
application: { "type": "m.call", "m.call.id": "", "m.call.intent": "voice" },
@@ -192,29 +253,34 @@ describe("CallMembership", () => {
it("rejects membership with no slot_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
}).toThrow();
});
it("rejects membership with invalid slot_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "invalid_slot_id" });
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "invalid_slot_id" });
}).toThrow();
});
it("rejects membership with slot_id that contains extra #", () => {
expect(() => {
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#mycall#extra" });
}).toThrow();
});
it("accepts membership with valid slot_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#" });
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#" });
}).not.toThrow();
});
it("rejects membership with no application", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
createCallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
}).toThrow();
});
it("rejects membership with incorrect application", () => {
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
application: { wrong_type_key: "unknown" },
});
@@ -223,34 +289,34 @@ describe("CallMembership", () => {
it("rejects membership with no member", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
}).toThrow();
});
it("rejects membership with incorrect member", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id_wrong: "test" },
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" },
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id: "@@test" },
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id: "@test-wrong-user:user.id" },
});
@@ -258,41 +324,41 @@ describe("CallMembership", () => {
});
it("rejects membership with incorrect sticky_key", () => {
expect(() => {
new CallMembership(makeMockEvent(), membershipTemplate);
createCallMembership(makeMockEvent(), membershipTemplate);
}).not.toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
sticky_key: 1,
msc4354_sticky_key: undefined,
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
sticky_key: "1",
msc4354_sticky_key: undefined,
});
}).not.toThrow();
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, msc4354_sticky_key: undefined });
createCallMembership(makeMockEvent(), { ...membershipTemplate, msc4354_sticky_key: undefined });
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
msc4354_sticky_key: 1,
sticky_key: "valid",
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
msc4354_sticky_key: "valid",
sticky_key: "valid",
});
}).not.toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
msc4354_sticky_key: "valid_but_different",
sticky_key: "valid",
@@ -300,21 +366,17 @@ describe("CallMembership", () => {
}).toThrow();
});
it("considers memberships unexpired if local age low enough", () => {
// TODO link prev event
});
it("considers memberships expired if local age large enough", () => {
// TODO link prev event
});
// TODO link prev event
it.todo("considers memberships unexpired if local age low enough");
it.todo("considers memberships expired if local age large enough");
describe("getTransport", () => {
it("gets the correct active transport with oldest_membership", () => {
const oldestMembership = new CallMembership(makeMockEvent(), {
const oldestMembership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
rtc_transports: [{ type: "oldest_transport" }],
});
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });
@@ -324,7 +386,7 @@ describe("CallMembership", () => {
});
});
describe("correct values from computed fields", () => {
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org");
});
@@ -363,28 +425,9 @@ describe("CallMembership", () => {
expect(membership.isExpired()).toBe(false);
});
});
});
describe("expiry calculation", () => {
let fakeEvent: MatrixEvent;
let membership: CallMembership;
beforeEach(() => {
// server origin timestamp for this event is 1000
fakeEvent = makeMockEvent(1000);
membership = new CallMembership(fakeEvent!, membershipTemplate);
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it("calculates time until expiry", () => {
jest.setSystemTime(2000);
// should be using absolute expiry time
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
it("uses unpadded base64 for RTC backend identities", async () => {
const membership = await CallMembership.parseFromEvent(makeMockEvent(0, { ...membershipTemplate }));
expect(membership.rtcBackendIdentity).toBe("jUZ0Q1yF5nV3LlAI5xfD1I7BPnAytJaPEAR57EXjJ6s");
});
});
});
File diff suppressed because it is too large Load Diff
@@ -14,27 +14,55 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ClientEvent, EventTimeline, MatrixClient, type Room } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { makeMockRoom, type MembershipData, membershipTemplate, mockRoomState, mockRTCEvent } from "./mocks";
import { ClientEvent, EventTimeline, MatrixClient, type Room, RoomStateEvent } from "../../../src";
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc";
import {
makeMockRoom,
type MembershipData,
sessionMembershipTemplate,
mockRoomState,
mockRTCEvent,
rtcMembershipTemplate,
} from "./mocks.ts";
import { logger } from "../../../src/logger";
import { flushPromises } from "../../test-utils/flushPromises";
import { type RtcMembershipData, type SessionMembershipData } from "../../../src/matrixrtc/membershipData";
describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
"MatrixRTCSessionManager ($eventKind)",
({ eventKind }) => {
let client: MatrixClient;
function sendLeaveMembership(room: Room, membershipData: MembershipData[]): void {
function generateMembership(opts: { type: string; callId?: string } = { type: "m.call" }): MembershipData {
if (eventKind === "sticky") {
return {
...rtcMembershipTemplate,
slot_id: opts.callId ? `${opts.type}#${opts.callId}` : rtcMembershipTemplate.slot_id,
application: {
...rtcMembershipTemplate.application,
type: opts.type,
},
} satisfies RtcMembershipData & { user_id: string };
}
return {
...sessionMembershipTemplate,
application: opts.type,
call_id: opts.callId ?? sessionMembershipTemplate.call_id, // approximate version.
} satisfies SessionMembershipData & { user_id: string };
}
async function sendLeaveMembership(room: Room, membershipData: MembershipData[]): Promise<void> {
if (eventKind === "memberState") {
mockRoomState(room, [{ user_id: membershipTemplate.user_id }]);
mockRoomState(room, [{ user_id: sessionMembershipTemplate.user_id }]);
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
} else {
membershipData.splice(0, 1, { user_id: membershipTemplate.user_id });
membershipData.splice(0, 1, { user_id: sessionMembershipTemplate.user_id });
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
}
await flushPromises();
}
beforeEach(() => {
@@ -45,30 +73,26 @@ describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
afterEach(() => {
client.stopClient();
client.matrixRTC.stop();
vi.resetAllMocks();
});
it("Fires event when session starts", () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
try {
const room1 = makeMockRoom([membershipTemplate], eventKind === "sticky");
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 starts", async () => {
const room1 = makeMockRoom([generateMembership({ type: "m.call" })], eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
const sessionStartedPromise = new Promise((resolve) =>
client.matrixRTC.once(MatrixRTCSessionManagerEvents.SessionStarted, resolve),
);
client.emit(ClientEvent.Room, room1);
await expect(sessionStartedPromise).resolves.toBeTruthy();
});
it("Doesn't fire event if unrelated sessions starts", () => {
const onStarted = jest.fn();
const onStarted = vi.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }], eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
const room1 = makeMockRoom([generateMembership({ type: "m.other" })], eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
@@ -77,23 +101,30 @@ describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
}
});
it("Fires event when session ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const membershipData: MembershipData[] = [membershipTemplate];
it("Fires event when session ends", async () => {
const sessionStartedPromise = new Promise((resolve) =>
client.matrixRTC.once(MatrixRTCSessionManagerEvents.SessionStarted, resolve),
);
const sessionEndedPromise = new Promise((resolve) =>
client.matrixRTC.once(MatrixRTCSessionManagerEvents.SessionEnded, (...params) => resolve(params)),
);
const membershipData: MembershipData[] = [generateMembership()];
const room1 = makeMockRoom(membershipData, eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
vi.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
await sessionStartedPromise;
await sendLeaveMembership(room1, membershipData);
sendLeaveMembership(room1, membershipData);
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
await expect(sessionEndedPromise).resolves.toStrictEqual([
room1.roomId,
client.matrixRTC.getActiveRoomSession(room1),
]);
});
it("Fires correctly with custom sessionDescription", () => {
const onStarted = jest.fn();
const onEnded = jest.fn();
it("Fires correctly with custom sessionDescription", async () => {
const onStarted = vi.fn();
const onEnded = vi.fn();
// create a session manager with a custom session description
const sessionManager = new MatrixRTCSessionManager(logger, client, {
id: "test",
@@ -104,53 +135,51 @@ describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
sessionManager.start();
sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
const sessionStartedPromise = new Promise((resolve) =>
sessionManager.once(MatrixRTCSessionManagerEvents.SessionStarted, resolve),
);
const sessionEndedPromise = new Promise((resolve) =>
sessionManager.once(MatrixRTCSessionManagerEvents.SessionEnded, (...params) => resolve(params)),
);
try {
// Create a session for applicaation m.other, we ignore this session ecause it lacks a call_id
const room1MembershipData: MembershipData[] = [{ ...membershipTemplate, application: "m.other" }];
const room1 = makeMockRoom(room1MembershipData, eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
onStarted.mockClear();
// Create a session for applicaation m.other, we ignore this session because it lacks a call_id
const room1MembershipData: MembershipData[] = [generateMembership({ type: "m.other" })];
const room1 = makeMockRoom(room1MembershipData, eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
await flushPromises();
expect(onStarted).not.toHaveBeenCalled();
// Create a session for applicaation m.notCall. We expect this call to be tracked because it has a call_id
const room2MembershipData: MembershipData[] = [
{ ...membershipTemplate, application: "m.notCall", call_id: "test" },
];
const room2 = makeMockRoom(room2MembershipData, eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
client.emit(ClientEvent.Room, room2);
expect(onStarted).toHaveBeenCalled();
onStarted.mockClear();
// Create a session for applicaation m.notCall. We expect this call to be tracked because it has matching call_id
const room2MembershipData: MembershipData[] = [generateMembership({ type: "m.notCall", callId: "test" })];
const room2 = makeMockRoom(room2MembershipData, eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room2]);
client.emit(ClientEvent.Room, room2);
await flushPromises();
await sessionStartedPromise;
// Stop room1's RTC session. Tracked.
jest.spyOn(client, "getRoom").mockReturnValue(room2);
sendLeaveMembership(room2, room2MembershipData);
expect(onEnded).toHaveBeenCalled();
onEnded.mockClear();
// Stop room1's RTC session. Not tracked.
vi.spyOn(client, "getRoom").mockReturnValue(room1);
await sendLeaveMembership(room1, room1MembershipData);
expect(onEnded).not.toHaveBeenCalled();
// Stop room1's RTC session. Not tracked.
jest.spyOn(client, "getRoom").mockReturnValue(room1);
sendLeaveMembership(room1, room1MembershipData);
expect(onEnded).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
}
// Stop room2's RTC session. Tracked.
vi.spyOn(client, "getRoom").mockReturnValue(room2);
await sendLeaveMembership(room2, room2MembershipData);
await sessionEndedPromise;
});
it("Doesn't fire event if unrelated sessions ends", () => {
const onEnded = jest.fn();
it("Doesn't fire event if unrelated sessions ends", async () => {
const onEnded = vi.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const membership: MembershipData[] = [{ ...membershipTemplate, application: "m.other_app" }];
const membership: MembershipData[] = [generateMembership({ type: "m.other_app" })];
const room1 = makeMockRoom(membership, eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
vi.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
sendLeaveMembership(room1, membership);
await sendLeaveMembership(room1, membership);
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
@@ -0,0 +1,27 @@
/*
Copyright 2026 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 { computeRtcIdentityRaw } from "../../../src/matrixrtc/membershipData/index.ts";
describe("computeRtcIdentityRaw", () => {
it("should compute the correct identity hash", async () => {
// Test vector taken from the spec, with the expected output updated to match the unpadded base64 encoding
// https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md#appendix-hash-derivation-test-vectors
const result = await computeRtcIdentityRaw("@alice:example.com", "DEVICE123", "memberABC");
// Add assertions based on expected hash output
expect(result).toBe("J+T45tGruxc+HrUOqJJlyQSV33m728Cme4+vt8/SWrU");
});
});
+196 -115
View File
@@ -1,5 +1,5 @@
/*
Copyright 2025 The Matrix.org Foundation C.I.C.
Copyright 2025-2026 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.
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MockedFunction, type Mock } from "jest-mock";
import { type Mock, type MockedFunction } from "vitest";
import {
type EmptyObject,
@@ -25,15 +25,16 @@ import {
type Room,
MAX_STICKY_DURATION_MS,
} from "../../../src";
import { MembershipManagerEvent, Status, type Transport, type LivekitFocusSelection } from "../../../src/matrixrtc";
import {
MembershipManagerEvent,
Status,
type Transport,
type SessionMembershipData,
type LivekitFocusSelection,
} from "../../../src/matrixrtc";
import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
makeMockClient,
makeMockRoom,
sessionMembershipTemplate,
mockCallMembership,
type MockClient,
} from "./mocks.ts";
import { MembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
import { type SessionMembershipData } from "../../../src/matrixrtc/membershipData/index.ts";
/**
* Create a promise that will resolve once a mocked method is called.
@@ -41,7 +42,7 @@ import { MembershipManager, StickyEventMembershipManager } from "../../../src/ma
* @param returnVal Provide an optional value that the mocked method should return. (use Promise.resolve(val) or Promise.reject(err))
* @returns The promise that resolves once the method is called.
*/
function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>): Promise<void> {
function waitForMockCall(method: MockedFunction<(...args: any[]) => any>, returnVal?: Promise<any>): Promise<void> {
const { promise, resolve } = Promise.withResolvers<void>();
method.mockImplementation(() => {
resolve();
@@ -51,7 +52,7 @@ function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>):
}
/** See waitForMockCall */
function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<any>) {
function waitForMockCallOnce(method: MockedFunction<(...args: any[]) => any>, returnVal?: Promise<any>) {
const { promise, resolve } = Promise.withResolvers<void>();
method.mockImplementationOnce(() => {
resolve();
@@ -65,13 +66,13 @@ function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<an
* @param method The method to control the resolve timing.
* @returns
*/
function createAsyncHandle<T>(method: MockedFunction<any>) {
function createAsyncHandle<T>(method: MockedFunction<(...args: any[]) => any>) {
const { reject, resolve, promise } = Promise.withResolvers<T>();
method.mockImplementation(() => promise);
return { reject, resolve };
}
const callSession = { id: "", application: "m.call" };
const callSession = { id: "ROOM", application: "m.call" };
describe("MembershipManager", () => {
let client: MockClient;
@@ -88,22 +89,22 @@ describe("MembershipManager", () => {
beforeEach(() => {
// Default to fake timers.
jest.useFakeTimers();
vi.useFakeTimers();
client = makeMockClient("@alice:example.org", "AAAAAAA");
room = makeMockRoom([membershipTemplate]);
room = makeMockRoom([sessionMembershipTemplate]);
// Provide a default mock that is like the default "non error" server behaviour.
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client._unstable_cancelScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
vi.mocked(client._unstable_sendDelayedStateEvent).mockResolvedValue({ delay_id: "id" });
vi.mocked(client._unstable_updateDelayedEvent).mockResolvedValue({});
vi.mocked(client._unstable_cancelScheduledDelayedEvent).mockResolvedValue({});
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockResolvedValue({});
vi.mocked(client._unstable_sendScheduledDelayedEvent).mockResolvedValue({});
vi.mocked(client._unstable_sendStickyEvent).mockResolvedValue({ event_id: "id" });
vi.mocked(client._unstable_sendStickyDelayedEvent).mockResolvedValue({ delay_id: "id" });
vi.mocked(client.sendStateEvent).mockResolvedValue({ event_id: "id" });
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
// There is no need to clean up mocks since we will recreate the client.
});
@@ -126,7 +127,7 @@ describe("MembershipManager", () => {
// Spys/Mocks
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
client._unstable_restartScheduledDelayedEvent as Mock,
client._unstable_restartScheduledDelayedEvent,
);
// Test
@@ -139,13 +140,16 @@ describe("MembershipManager", () => {
"org.matrix.msc3401.call.member",
{
application: "m.call",
// This tests INFO_SLOT_ID_LEGACY_CASE because it is using callSession = { id: "ROOM", application: "m.call" }
call_id: "",
device_id: "AAAAAAA",
expires: 14400000,
foci_preferred: [focus],
membershipID: "@alice:example.org:AAAAAAA",
focus_active: focusActive,
scope: "m.room",
},
// This tests INFO_SLOT_ID_LEGACY_CASE because it is using callSession = { id: "ROOM", application: "m.call" }
"_@alice:example.org_AAAAAAA_m.call",
);
restartScheduledDelayedEventHandle.resolve?.();
@@ -159,6 +163,45 @@ describe("MembershipManager", () => {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("sends correct call_id and state key when using non empty string. Not using empty string -> ROOM hack. See: INFO_SLOT_ID_LEGACY_CASE", async () => {
// Spys/Mocks
const customCallSession = { id: "custom", application: "m.call" };
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
client._unstable_restartScheduledDelayedEvent,
);
// Test
const memberManager = new MembershipManager(undefined, room, client, customCallSession);
memberManager.join([focus], undefined);
// expects
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
"org.matrix.msc3401.call.member",
{
application: "m.call",
call_id: "custom",
device_id: "AAAAAAA",
expires: 14400000,
foci_preferred: [focus],
membershipID: "@alice:example.org:AAAAAAA",
focus_active: focusActive,
scope: "m.room",
},
"_@alice:example.org_AAAAAAA_m.callcustom",
);
restartScheduledDelayedEventHandle.resolve?.();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
room.roomId,
{ delay: 8000 },
"org.matrix.msc3401.call.member",
{},
"_@alice:example.org_AAAAAAA_m.callcustom",
);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("reschedules delayed leave event if sending state cancels it", async () => {
const memberManager = new MembershipManager(undefined, room, client, callSession);
const waitForSendState = waitForMockCall(client.sendStateEvent);
@@ -169,7 +212,7 @@ describe("MembershipManager", () => {
memberManager.join([focus], focusActive);
await waitForSendState;
await waitForRestartScheduledDelayedEvent;
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
// Once for the initial event and once because of the errcode: "M_NOT_FOUND"
// Different to "sends a membership event and schedules delayed leave when joining a call" where its only called once (1)
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
@@ -182,7 +225,7 @@ describe("MembershipManager", () => {
// - run into rate limit for sending delayed event
// - run into rate limit when setting membership state.
if (useOwnedStateEvents) {
room.getVersion = jest.fn().mockReturnValue("org.matrix.msc3757.default");
room.getVersion = vi.fn().mockReturnValue("org.matrix.msc3757.default");
}
const restartScheduledDelayedEvent = waitForMockCall(client._unstable_restartScheduledDelayedEvent);
const sentDelayedState = waitForMockCall(
@@ -199,7 +242,7 @@ describe("MembershipManager", () => {
"org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
"org.matrix.msc4140.max_delay": 7500,
});
(client._unstable_sendDelayedStateEvent as Mock).mockImplementationOnce(() => {
vi.mocked(client._unstable_sendDelayedStateEvent).mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
@@ -209,7 +252,7 @@ describe("MembershipManager", () => {
// preparing the delayed disconnect should handle ratelimiting
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
(client._unstable_sendDelayedStateEvent as Mock).mockImplementationOnce(() => {
vi.mocked(client._unstable_sendDelayedStateEvent).mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
@@ -224,7 +267,7 @@ describe("MembershipManager", () => {
undefined,
new Headers({ "Retry-After": "1" }),
);
(client.sendStateEvent as Mock).mockImplementationOnce(() => {
vi.mocked(client.sendStateEvent).mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
@@ -247,11 +290,11 @@ describe("MembershipManager", () => {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(1, ...callProps(9000));
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(2, ...callProps(7500));
await jest.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(5000);
await sendStateEventAttempt.then(); // needed to resolve after resendIfRateLimited catches
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
expect(client.sendStateEvent).toHaveBeenCalledWith(
room!.roomId,
@@ -263,6 +306,7 @@ describe("MembershipManager", () => {
expires: 14400000,
device_id: "AAAAAAA",
foci_preferred: [focus],
membershipID: "@alice:example.org:AAAAAAA",
focus_active: focusActive,
} satisfies SessionMembershipData,
userStateKey,
@@ -274,15 +318,17 @@ describe("MembershipManager", () => {
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
// ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers.
await jest.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(5000);
// should update delayed disconnect
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
}
// eslint-disable-next-line @vitest/expect-expect
it("sends a membership event after rate limits during delayed event setup when joining a call", async () => {
await testJoin(false);
});
// eslint-disable-next-line @vitest/expect-expect
it("does not prefix the state key with _ for rooms that support user-owned state events", async () => {
await testJoin(true);
});
@@ -291,7 +337,7 @@ describe("MembershipManager", () => {
describe("delayed leave event", () => {
it("does not try again to schedule a delayed leave event if not supported", () => {
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
delayedHandle.reject?.(
@@ -303,11 +349,11 @@ describe("MembershipManager", () => {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("does try to schedule a delayed leave event again if rate limited", async () => {
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
delayedHandle.reject?.(new HTTPError("rate limited", 429, undefined));
await jest.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(5000);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
});
it("uses delayedLeaveEventDelayMs from config", () => {
@@ -336,7 +382,7 @@ describe("MembershipManager", () => {
manager.join([focus]);
expect(manager.status).toBe(Status.Connecting);
// Let the scheduler run one iteration so that we can send the join state event
await jest.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(manager.status).toBe(Status.Connected);
// Now that we are connected, we set up the mocks.
@@ -353,12 +399,12 @@ describe("MembershipManager", () => {
);
const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent);
await jest.advanceTimersByTimeAsync(RESTART_DELAY);
await vi.advanceTimersByTimeAsync(RESTART_DELAY);
// first simulate the sync, then resolve sending the delayed event.
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
resolve({ delay_id: "id" });
// Let the scheduler run one iteration so that the new join gets sent
await jest.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
});
@@ -383,6 +429,7 @@ describe("MembershipManager", () => {
device_id: "AAAAAAA",
expires: 1234567,
foci_preferred: [focus],
membershipID: "@alice:example.org:AAAAAAA",
focus_active: {
focus_selection: "oldest_membership",
type: "livekit",
@@ -407,15 +454,16 @@ describe("MembershipManager", () => {
it("resolves delayed leave event when leave is called", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
await manager.leave();
expect(client._unstable_sendScheduledDelayedEvent).toHaveBeenLastCalledWith("id");
expect(client.sendStateEvent).toHaveBeenCalled();
expect(manager.delayId).toBe(undefined);
});
it("send leave event when leave is called and resolving delayed leave fails", async () => {
it("send leave event when leave is called and resolving delayed leave fails unknown error", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue("unknown");
await manager.leave();
@@ -426,10 +474,31 @@ describe("MembershipManager", () => {
{},
"_@alice:example.org_AAAAAAA_m.call",
);
// If there is a unknown error, we do not reset the delayId
// The delayed event might still be around and we track it.
expect(manager.delayId).not.toBe(undefined);
});
it("does nothing if not joined", () => {
it("send leave event when leave is called and resolving delayed leave fails not found error", async () => {
const manager = new MembershipManager({}, room, client, callSession);
expect(async () => await manager.leave()).not.toThrow();
manager.join([focus]);
await vi.advanceTimersByTimeAsync(1);
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue(
new MatrixError({ errcode: "M_NOT_FOUND" }, 404),
);
await manager.leave();
// We send a normal leave event since we failed using sendScheduledDelayedEvent.
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
room.roomId,
"org.matrix.msc3401.call.member",
{},
"_@alice:example.org_AAAAAAA_m.call",
);
expect(manager.delayId).toBe(undefined);
});
it("does nothing if not joined", async () => {
const manager = new MembershipManager({}, room, client, callSession);
await expect(manager.leave()).resolves.toBeTruthy();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client.sendStateEvent).not.toHaveBeenCalled();
});
@@ -438,8 +507,8 @@ describe("MembershipManager", () => {
describe("onRTCSessionMemberUpdate()", () => {
it("does nothing if not joined", async () => {
const manager = new MembershipManager({}, room, client, callSession);
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await jest.advanceTimersToNextTimerAsync();
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
await vi.advanceTimersToNextTimerAsync();
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
@@ -450,25 +519,25 @@ describe("MembershipManager", () => {
it("does nothing if own membership still present", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
const myMembership = (client.sendStateEvent as Mock).mock.calls[0][2];
await vi.advanceTimersByTimeAsync(1);
const myMembership = vi.mocked(client.sendStateEvent).mock.calls[0][2];
// reset all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
(client.sendStateEvent as Mock).mockClear();
(client._unstable_updateDelayedEvent as Mock).mockClear();
(client._unstable_cancelScheduledDelayedEvent as Mock).mockClear();
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
(client._unstable_sendScheduledDelayedEvent as Mock).mockClear();
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
vi.mocked(client.sendStateEvent).mockClear();
vi.mocked(client._unstable_updateDelayedEvent).mockClear();
vi.mocked(client._unstable_cancelScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_sendScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_sendDelayedStateEvent).mockClear();
await manager.onRTCSessionMemberUpdate([
mockCallMembership(membershipTemplate, room.roomId),
mockCallMembership(sessionMembershipTemplate, room.roomId),
mockCallMembership(
{ ...(myMembership as SessionMembershipData), user_id: client.getUserId()! },
room.roomId,
),
]);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
@@ -480,15 +549,15 @@ describe("MembershipManager", () => {
it("recreates membership if it is missing", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
(client.sendStateEvent as Mock).mockClear();
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
vi.mocked(client.sendStateEvent).mockClear();
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_sendDelayedStateEvent).mockClear();
// Our own membership is removed:
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await jest.advanceTimersByTimeAsync(1);
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
await vi.advanceTimersByTimeAsync(1);
expect(client.sendStateEvent).toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
@@ -498,21 +567,21 @@ describe("MembershipManager", () => {
it("updates the UpdateExpiry entry in the action scheduler", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
(client.sendStateEvent as Mock).mockClear();
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
vi.mocked(client.sendStateEvent).mockClear();
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_sendDelayedStateEvent).mockClear();
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValueOnce(
new MatrixError({ errcode: "M_NOT_FOUND" }),
);
const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent);
await jest.advanceTimersByTimeAsync(10_000);
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await vi.advanceTimersByTimeAsync(10_000);
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
resolve({ delay_id: "id" });
await jest.advanceTimersByTimeAsync(10_000);
await vi.advanceTimersByTimeAsync(10_000);
expect(client.sendStateEvent).toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
@@ -532,7 +601,7 @@ describe("MembershipManager", () => {
{ id: "", application: "m.call" },
);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
// The first call is from checking id the server deleted the delayed event
@@ -543,7 +612,7 @@ describe("MembershipManager", () => {
for (let i = 2; i <= 12; i++) {
// flush promises before advancing the timers to make sure schedulers are setup
await jest.advanceTimersByTimeAsync(10_000);
await vi.advanceTimersByTimeAsync(10_000);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(i);
// TODO: Check that update delayed event is called with the correct HTTP request timeout
@@ -566,18 +635,20 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive);
await waitForMockCall(client.sendStateEvent);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
const sentMembership = (client.sendStateEvent as Mock).mock.calls[0][2] as SessionMembershipData;
const sentMembership = vi.mocked(client.sendStateEvent).mock.calls[0][2] as SessionMembershipData;
expect(sentMembership.expires).toBe(expire);
for (let i = 2; i <= 12; i++) {
await jest.advanceTimersByTimeAsync(expire);
await vi.advanceTimersByTimeAsync(expire);
expect(client.sendStateEvent).toHaveBeenCalledTimes(i);
const sentMembership = (client.sendStateEvent as Mock).mock.lastCall![2] as SessionMembershipData;
const sentMembership = vi.mocked(client.sendStateEvent).mock.lastCall![2] as SessionMembershipData;
expect(sentMembership.expires).toBe(expire * i);
}
}
// eslint-disable-next-line @vitest/expect-expect
it("extends `expires` when call still active", async () => {
await testExpires(10_000);
});
// eslint-disable-next-line @vitest/expect-expect
it("extends `expires` using headroom configuration", async () => {
await testExpires(10_000, 1_000);
});
@@ -594,23 +665,23 @@ describe("MembershipManager", () => {
const manager = new MembershipManager({}, room, client, callSession);
expect(manager.status).toBe(Status.Disconnected);
const connectEmit = jest.fn();
const connectEmit = vi.fn();
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
manager.join([focus], focusActive);
expect(manager.status).toBe(Status.Connecting);
handleDelayedEvent.resolve();
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(connectEmit).toHaveBeenCalledWith(Status.Disconnected, Status.Connecting);
handleStateEvent.resolve();
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected);
});
it("emits 'Disconnecting' and 'Disconnected' after leave", async () => {
const manager = new MembershipManager({}, room, client, callSession);
const connectEmit = jest.fn();
const connectEmit = vi.fn();
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
await manager.leave();
expect(connectEmit).toHaveBeenCalledWith(Status.Connected, Status.Disconnecting);
expect(connectEmit).toHaveBeenCalledWith(Status.Disconnecting, Status.Disconnected);
@@ -635,7 +706,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "1" }),
),
);
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
});
@@ -653,7 +724,7 @@ describe("MembershipManager", () => {
// Should call _unstable_sendDelayedStateEvent but not sendStateEvent because of the
// RateLimit error.
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
@@ -661,7 +732,7 @@ describe("MembershipManager", () => {
// the membership is no longer present on the homeserver
await manager.onRTCSessionMemberUpdate([]);
// Wait for all timers to be setup
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
// We should send the first own membership and a new delayed event after the rate limit timeout.
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
@@ -681,13 +752,13 @@ describe("MembershipManager", () => {
),
);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
// the user terminated the call locally
await manager.leave();
// Wait for all timers to be setup
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
// No new events should have been sent:
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
@@ -708,16 +779,16 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive);
// Hit rate limit
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
// Hit second rate limit.
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
// Setup resolve
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
@@ -727,7 +798,7 @@ describe("MembershipManager", () => {
describe("unrecoverable errors", () => {
// because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
it("throws, when reaching maximum number of retries for initial delayed event creation", async () => {
const delayEventSendError = jest.fn();
const delayEventSendError = vi.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
@@ -741,13 +812,13 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive, delayEventSendError);
for (let i = 0; i < 10; i++) {
await jest.advanceTimersByTimeAsync(2000);
await vi.advanceTimersByTimeAsync(2000);
}
expect(delayEventSendError).toHaveBeenCalled();
});
// because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
it("throws, when reaching maximum number of retries", async () => {
const delayEventRestartError = jest.fn();
const delayEventRestartError = vi.fn();
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
@@ -761,12 +832,12 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive, delayEventRestartError);
for (let i = 0; i < 10; i++) {
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
}
expect(delayEventRestartError).toHaveBeenCalled();
});
it("falls back to using pure state events when some error occurs while sending delayed events", async () => {
const unrecoverableError = jest.fn();
const unrecoverableError = vi.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601));
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive, unrecoverableError);
@@ -775,7 +846,7 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).toHaveBeenCalled();
});
it("retries before failing in case its a network error", async () => {
const unrecoverableError = jest.fn();
const unrecoverableError = vi.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 501));
const manager = new MembershipManager(
{ networkErrorRetryMs: 1000, maximumNetworkErrorRetryCount: 7 },
@@ -786,7 +857,7 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive, unrecoverableError);
for (let retries = 0; retries < 7; retries++) {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(retries + 1);
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
}
expect(unrecoverableError).toHaveBeenCalled();
expect(unrecoverableError.mock.lastCall![0].message).toMatch(
@@ -795,13 +866,13 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).not.toHaveBeenCalled();
});
it("falls back to using pure state events when UnsupportedDelayedEventsEndpointError encountered for delayed events", async () => {
const unrecoverableError = jest.fn();
const unrecoverableError = vi.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
new UnsupportedDelayedEventsEndpointError("not supported", "sendDelayedStateEvent"),
);
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive, unrecoverableError);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(unrecoverableError).not.toHaveBeenCalled();
expect(client.sendStateEvent).toHaveBeenCalled();
@@ -817,7 +888,7 @@ describe("MembershipManager", () => {
callSession,
);
const { promise: stuckPromise, reject: rejectStuckPromise } = Promise.withResolvers<EmptyObject>();
const probablyLeftEmit = jest.fn();
const probablyLeftEmit = vi.fn();
manager.on(MembershipManagerEvent.ProbablyLeft, probablyLeftEmit);
manager.join([focus], focusActive);
try {
@@ -826,19 +897,19 @@ describe("MembershipManager", () => {
// We never resolve the delayed event so that we can test the probablyLeft event.
// This simulates the case where the server does not respond to the delayed event.
client._unstable_restartScheduledDelayedEvent = jest.fn(() => stuckPromise);
client._unstable_restartScheduledDelayedEvent = vi.fn((_) => stuckPromise);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(manager.status).toBe(Status.Connected);
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
// We expect the probablyLeft event to be emitted after the `delayedLeaveEventDelayMs` = 10000.
// We also track the calls to updated the delayed event that all will never resolve to simulate the server not responding.
// The numbers are a bit arbitrary since we use the local timeout that does not perfectly match the 5s check interval in this test.
await jest.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(5000);
// No emission after 5s
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(4999);
await vi.advanceTimersByTimeAsync(4999);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
@@ -846,14 +917,14 @@ describe("MembershipManager", () => {
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue({});
// Emit after 10s
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(4);
expect(probablyLeftEmit).toHaveBeenCalledWith(true);
// Mock a sync which does not include our own membership
await manager.onRTCSessionMemberUpdate([]);
// Wait for the current ongoing delayed event sending to finish
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
// We should send a new state event and an associated delayed leave event.
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
@@ -866,6 +937,7 @@ describe("MembershipManager", () => {
});
describe("updateCallIntent()", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should fail if the user has not joined the call", async () => {
const manager = new MembershipManager({}, room, client, callSession);
// After joining we want our own focus to be the one we select.
@@ -879,11 +951,14 @@ describe("MembershipManager", () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([]);
expect(manager.isActivated()).toEqual(true);
const membership = mockCallMembership({ ...membershipTemplate, user_id: client.getUserId()! }, room.roomId);
const membership = mockCallMembership(
{ ...sessionMembershipTemplate, user_id: client.getUserId()! },
room.roomId,
);
await manager.onRTCSessionMemberUpdate([membership]);
await manager.updateCallIntent("video");
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
const eventContent = (client.sendStateEvent as Mock).mock.calls[0][2] as SessionMembershipData;
const eventContent = vi.mocked(client.sendStateEvent).mock.calls[0][2] as SessionMembershipData;
expect(eventContent["created_ts"]).toEqual(membership.createdTs());
expect(eventContent["m.call.intent"]).toEqual("video");
});
@@ -893,7 +968,7 @@ describe("MembershipManager", () => {
manager.join([]);
expect(manager.isActivated()).toEqual(true);
const membership = mockCallMembership(
{ ...membershipTemplate, "user_id": client.getUserId()!, "m.call.intent": "video" },
{ ...sessionMembershipTemplate, "user_id": client.getUserId()!, "m.call.intent": "video" },
room.roomId,
);
await manager.onRTCSessionMemberUpdate([membership]);
@@ -913,9 +988,15 @@ describe("MembershipManager", () => {
describe("sends an rtc membership event", () => {
it("sends a membership event and schedules delayed leave when joining a call", async () => {
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
client._unstable_restartScheduledDelayedEvent as Mock,
client._unstable_restartScheduledDelayedEvent,
);
const memberManager = new StickyEventMembershipManager(
undefined,
room,
client,
callSession,
"@alice:example.org:AAAAAAA_m.call",
);
const memberManager = new StickyEventMembershipManager(undefined, room, client, callSession);
memberManager.join([], focus);
@@ -930,13 +1011,13 @@ describe("MembershipManager", () => {
application: { type: "m.call" },
member: {
user_id: "@alice:example.org",
id: "_@alice:example.org_AAAAAAA_m.call",
id: "@alice:example.org:AAAAAAA_m.call",
device_id: "AAAAAAA",
},
slot_id: "m.call#",
rtc_transports: [focus],
slot_id: "m.call#ROOM",
rtc_transports: [{ type: focus.type, livekit_service_url: focus.livekit_service_url }],
versions: [],
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
},
);
restartScheduledDelayedEventHandle.resolve?.();
@@ -949,7 +1030,7 @@ describe("MembershipManager", () => {
null,
"org.matrix.msc4143.rtc.member",
{
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
},
);
// ..once
@@ -962,11 +1043,11 @@ describe("MembershipManager", () => {
it("Should prefix log with MembershipManager used", () => {
const client = makeMockClient("@alice:example.org", "AAAAAAA");
const room = makeMockRoom([membershipTemplate]);
const room = makeMockRoom([sessionMembershipTemplate]);
const membershipManager = new MembershipManager(undefined, room, client, callSession);
const spy = jest.spyOn(console, "error");
const spy = vi.spyOn(console, "error");
// Double join
membershipManager.join([]);
membershipManager.join([]);
@@ -25,9 +25,9 @@ describe("OutdatedKeyFilter Test", () => {
const olderKey = fakeInboundSessionWithTimestamp(300);
// Simulate receiving out of order keys
expect(filter.isOutdated(aKey.participantId, aKey)).toBe(false);
expect(filter.isOutdated(aKey.membership, aKey)).toBe(false);
// Then we receive the most recent key out of order
const isOutdated = filter.isOutdated(aKey.participantId, olderKey);
const isOutdated = filter.isOutdated(aKey.membership, olderKey);
// this key is older and should be ignored even if received after
expect(isOutdated).toBe(true);
});
@@ -36,7 +36,7 @@ describe("OutdatedKeyFilter Test", () => {
return {
keyIndex: 0,
creationTS: ts,
participantId: "@alice:localhost|ABCDE",
membership: { userId: "@alice:localhost", deviceId: "ABDE", memberId: "@alice:localhost:ABCDE" },
key: new Uint8Array(16),
};
}

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