Compare commits

...

357 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
RiotRobot dd2635dbe6 v39.3.0 2025-12-02 14:44:30 +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
RiotRobot 23241f18e2 v39.3.0-rc.0 2025-11-25 14:10:43 +00:00
Michael Telatynski 90da67aa95 Re-add truthy check on room name/avatar/alias events (#5081)
* Re-add truthy check on room name/avatar/alias events

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

* Add regression test

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-25 09:09:11 +00:00
renovate[bot] 0bf2702149 Update jest to v30 (major) (#4875)
* Update jest to v30

* Update snapshots & imports

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

* Make jest happier

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

* Fix tests

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-24 09:07:13 +00:00
RiotRobot c7a75c8824 Merge branch 'master' into develop 2025-11-18 14:27:42 +00:00
RiotRobot 98b2b9745d v39.2.0 2025-11-18 14:26:56 +00:00
Michael Telatynski 65d5b3172c Fix invalid state events corrupting room objects (#5078)
* Fix invalid room name/canonical alias corrupting room objects

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

* Iterate

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

* Add test

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-11-18 10:46:24 +00:00
renovate[bot] 2f72f9e889 Update dependency eslint-plugin-jsdoc to v61.1.12 (#5074)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 14:30:01 +00:00
renovate[bot] 18f500a1f8 Update all non-major dependencies (#5076)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 14:01:33 +00:00
renovate[bot] b1df58796a Update typescript-eslint monorepo to v8.46.3 (#5075)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 14:01:12 +00:00
RiotRobot 761b3771d6 v39.2.0-rc.0 2025-11-11 12:44:05 +00:00
Andrew Ferrazzutti df88edfda0 Delayed event management: split endpoints, no auth (#5066)
* Delayed event management: split endpoints, no auth

Add dedicated endpoints for each of the cancel/restart/send actions for
updating a delayed event, and make them unauthenticated.

Also keep support for the original endpoint where the update action is
in the request body, and make the split-endpoint versions fall back to
it if they are unsupported by the homeserver.

* Don't @link parameters in method docstrings

as TypeDoc doesn't support that

* Reduce code duplication

* Reduce code duplication again

* Add a little more test coverage

* Use split delayed event management for widgets

* Specify which eslint rule to ignore

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>

* Restore embedded non-split delay evt update method

Keep supporting it to not break widgets that currently use it.
Also add back the test for it.

* Deprecate the non-split delay evt update methods

* Comment to explain fallback to non-split endpoint

* Add backwards compatibility with authed endpoints

* Comment backwards compatibility helper method

* Await returned promises

because `return await promise` is at least as fast as `return promise`

---------

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>
2025-11-11 05:54:33 +00:00
David Langley 1dee1ba581 Fix media switching during legacy calls (#5069)
* Specify exact for deviceId

* Update mediaHandler.spec.ts

* fallback to ideal if exact fails

* Reduce cognitive complexity for sonar

* Add tests
2025-11-10 09:36:32 +00:00
pkuzco b274c74a30 do not set cache in authenticated fetch (#5020)
* do not set cache in authenticated fetch

* issue 5019 - updated comment

* do not set cache in authenticated fetch

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>

* issue 5019 - updated comment

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>

---------

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>
Co-authored-by: Bahaa Naamneh <glimm.no@gmail.com>
2025-11-06 14:36:27 +00:00
RiotRobot b489bb15cf Merge branch 'master' into develop 2025-11-04 14:02:00 +00:00
RiotRobot dff4922a42 v39.1.2 2025-11-04 14:01:14 +00:00
Michael Telatynski dc6ad0b54c Fix npm publish script (#5068)
The `id` is also an output from this workflow so needs re-adding, not sure how the token stuck around, I thought I removed that
2025-11-04 13:33:55 +00:00
RiotRobot 9769c05dc5 Merge branch 'master' into develop 2025-11-04 13:23:41 +00:00
RiotRobot dd379d3d4c v39.1.1 2025-11-04 13:22:58 +00:00
Michael Telatynski 1b884a3e52 Fix npm release missing template braces (#5067) 2025-11-04 13:00:29 +00:00
RiotRobot ddb164490e Merge branch 'master' into develop 2025-11-04 12:54:27 +00:00
RiotRobot 796135c7ce v39.1.0 2025-11-04 12:53:45 +00:00
Michael Telatynski 4cc4c01dd8 Remove call to npm dist-tag (#5065)
As it does not work for NPM Trusted Publishing

https://github.com/npm/cli/issues/8547

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-10-30 09:34:14 +00:00
RiotRobot 533b40922c v39.1.0-rc.1 2025-10-30 08:45:29 +00:00
Michael Telatynski 0ae483ce27 Use NPM Trusted Publishers over token (#5064)
* Use NPM Trusted Publishers over token

due to security changes being enacted next month by npm

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

* Update npm as ubuntu-latest only has 10.x and 11.5.1 or later is necessary

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-10-29 16:07:11 +00:00
renovate[bot] dbc1fa87ed Update dependency eslint-plugin-jsdoc to v61.1.8 (#5058)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 23:03:02 +00:00
renovate[bot] 0a3675b971 Update typescript-eslint monorepo to v8.46.2 (#5059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 16:11:40 +00:00
renovate[bot] ab3f529d29 Update babel monorepo to v7.28.5 (#5057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:57:44 +00:00
renovate[bot] 607b712a07 Update dependency sdp-transform to v3 (#5062)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:54:32 +00:00
renovate[bot] b6d9e49277 Update dependency @stylistic/eslint-plugin to v5.5.0 (#5061)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:54:17 +00:00
renovate[bot] 731d5943e2 Update GitHub Artifact Actions (#5063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:54:08 +00:00
renovate[bot] 01e7a43593 Update all non-major dependencies (#5060)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 14:53:35 +00:00
RiotRobot b69a19ce7c v39.1.0-rc.0 2025-10-28 14:03:05 +00:00
Valere Fedronic 8703acb533 MatrixRTC: Disable room transport fallback for keys (#4929)
* matrixRTC: Disable room transport fallback for keys

* post rebase fix

* post merge fix

---------

Co-authored-by: Will Hunt <will@half-shot.uk>
2025-10-28 09:03:15 +00:00
Timo b59603d748 [MatrixRTC] Sticky Events support (MSC4354) (#5017)
* Implement Sticky Events MSC

* Renames

* lint

* some review work

* Update for support for 4-ples

* fix lint

* pull through method

* Fix the mistake

* More tests to appease SC

* Cleaner code

* Review cleanup

* Refactors based on review.

* lint

* Add sticky event support to the js-sdk

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

* use sticky events for matrixRTC

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

* make sticky events a non breaking change (default to state events. use joinConfig to use sticky events)

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

* review
 - fix types (`msc4354_sticky:number` -> `msc4354_sticky?: { duration_ms: number };`)
  - add `MultiKeyMap`

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

* Refactor all of this away to it's own accumulator and class.

* Add tests

* tidyup

* more test cleaning

* lint

* Updates and tests

* fix filter

* fix filter with lint

* Add timer tests

* Add tests for MatrixRTCSessionManager

* Listen for sticky events on MatrixRTCSessionManager

* fix logic on filtering out state events

* lint

* more lint

* tweaks

* Add logging in areas

* more debugging

* much more logging

* remove more logging

* Finish supporting new MSC

* a line

* reconnect the bits to RTC

* fixup more bits

* fixup testrs

* Ensure consistent order

* lint

* fix log line

* remove extra bit of code

* revert changes to room-sticky-events.ts

* fixup mocks again

* lint

* fix

* cleanup

* fix paths

* tweak test

* fixup

* Add more tests for coverage

* Small improvements

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

* review

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

* Document better

* fix sticky event type

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

* fix demo

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

* fix tests

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

* Update src/matrixrtc/CallMembership.ts

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

* cleanup

* lint

* fix ci

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Half-Shot <will@half-shot.uk>
Co-authored-by: Robin <robin@robin.town>
2025-10-23 14:56:54 +00:00
Richard van der Hoff b0cbe22f64 Add CryptoApi.getSecretStorageStatus (#5054)
* Add `CryptoApi.getSecretStorageStatus`

`isSecretStorageReady` is a bit of a blunt instrument: it's hard to see from
logs *why* the secret storage isn't ready.

Add a new method which returns a bit more data.

* Update src/rust-crypto/rust-crypto.ts

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

---------

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2025-10-23 11:04:28 +00:00
Timo 977d0322da Add parseCallNotificationContent (#5015)
* add parseCallNotificationContent

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

* add tests

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

* remove decline reason and better m.mentions check

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

* cap ring duration to EX value (90s)

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-10-21 13:02:14 +00:00
RiotRobot dd7394c14c Merge branch 'master' into develop 2025-10-21 11:19:14 +00:00
RiotRobot f2d082064e v39.0.0 2025-10-21 11:18:29 +00:00
Andrew Ferrazzutti 2731e20893 MSC4140: support filters on delayed event lookup (#5038)
* MSC4140: support filters on delayed event lookup

Support looking up scheduled/finalised delayed events, and looking up a
single delayed event.

* Add test coverage for delayed event lookup filters

* Prettier

* Use it.each for test loop

* Support multiple delayIds

* Support single or multiple delayIds

As it may be more common to look up a single delayed event than to look
up many of them, support passing a single delayID in the lookup function
instead of needing to pass a single-element array.
2025-10-20 14:59:29 +00:00
renovate[bot] 502a513b5b Update eslint-plugins (#5050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 10:28:30 +00:00
renovate[bot] 3ac47e71cd Update eslint-plugins (#5049)
* Update eslint-plugins

* Bump matrix-org

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-10-15 09:49:42 +00:00
renovate[bot] 7c1e25e713 Update actions/setup-node action to v6 (#5047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 16:28:15 +00:00
renovate[bot] 2d90ad95f1 Update peter-evans/repository-dispatch action to v4 (#5048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:19:11 +00:00
renovate[bot] cd9794471f Update dependency @types/node to v18.19.130 (#5045)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:17:41 +00:00
renovate[bot] b2d3ab8bc1 Update typescript-eslint monorepo to v8.46.0 (#5046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:17:11 +00:00
renovate[bot] d8b70ef83b Update actions/stale digest to 5f858e3 (#5043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:16:55 +00:00
renovate[bot] a67fb1fb8d Update all non-major dependencies (#5044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 15:13:48 +00:00
Will Hunt ddd6e77cde Handle redactions for sticky events (MSC4354) (#5037)
* Handle redactions.

* Add unit tests

* Refactor to support unstable insertion orders.

* lint

* tidy tidy

* Fix doc

* lint

* Assert userId

* Catch one occurance of sticky event errors not being caught
2025-10-14 13:49:31 +00:00
RiotRobot 2e9f5b6033 v39.0.0-rc.0 2025-10-14 13:34:57 +00:00
Timo fd949fe486 [MatrixRTC] Multi SFU support + m.rtc.member event type support (#5022)
* WIP

* temp

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

* Fix imports

* Fix checkSessionsMembershipData thinking foci_preferred is required

* incorporate CallMembership changes
 - rename Focus -> Transport
 - add RtcMembershipData (next to `sessionMembershipData`)
 - make `new CallMembership` initializable with both
 - move oldest member calculation into CallMembership

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

* use correct event type

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

* fix sonar cube conerns

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

* callMembership tests

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

* make test correct

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

* make sonar cube happy (it does not know about the type constraints...)

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

* remove created_ts from RtcMembership

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

* fix imports

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

* Update src/matrixrtc/IMembershipManager.ts

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

* rename LivekitFocus.ts -> LivekitTransport.ts

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

* add details to `getTransport`

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

* review

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

* use DEFAULT_EXPIRE_DURATION in tests

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

* fix test `does not provide focus if the selection method is unknown`

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

* Update src/matrixrtc/CallMembership.ts

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

* Move `m.call.intent` into the `application` section for rtc member events.

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

* review on rtc object validation code.

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

* user id check

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

* review: Refactor RTC membership handling and improve error handling

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

* docstring updates

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

* add back deprecated `getFocusInUse` & `getActiveFocus`

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

* ci

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

* Update src/matrixrtc/CallMembership.ts

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

* lint

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

* make test less strict for ew tests

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

* Typescript downstream test adjustments

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

* err

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Robin <robin@robin.town>
2025-10-08 19:12:29 +00:00
Michael Telatynski 7b3aed8a47 Tidy knip config (#5036)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-10-08 14:19:25 +00:00
Will Hunt b84a73c7cc Implement Sticky Events MSC4354 (#5028)
* Implement Sticky Events MSC

* Renames

* lint

* some review work

* Update for support for 4-ples

* fix lint

* pull through method

* Fix the mistake

* More tests to appease SC

* Cleaner code

* Review cleanup

* Refactors based on review.

* lint

* Store sticky event expiry TS at insertion time.

* proper type
2025-10-07 17:24:10 +00:00
Michael Telatynski a03cf054a8 Only use the first 3 viaServers specified (#5034)
* Only use the first 3 viaServers specified

To avoid HTTP 414 URI Too Long error

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>
2025-10-07 13:14:13 +00:00
RiotRobot b3d217717a Merge branch 'master' into develop 2025-10-07 12:06:14 +00:00
RiotRobot 3e6fe5f914 v38.4.0 2025-10-07 12:05:30 +00:00
Andy Balaam a213d177f9 Fetch the user's device info before processing a verification request (#5030)
* Use checked way to get OlmMachine

* Factor out two variables in onKeyVerificationEvent

* Make sure verification test waits for the request to be processed

* Fetch the user's device info before processing a verification request

If we don't have the device info for a user when we receive their
verification request, we ignore it. This change gives us the best
possible chance of having the right device data before we try to process
the verification.

Fixes #30693
Fixes #27819
2025-10-07 08:13:31 +00:00
Will Hunt e885ecf08d Stabilise extended profiles (now part of Matrix v1.16) (#5013)
* Stabilise extended profiles (v1.16)

* lint

* Add capacity

* Update profile keys

* copyright

* lint
2025-10-06 15:10:15 +00:00
pkuzco d1d9aba745 feat(client): allow disabling VoIP support (#5021)
* feat(client): allow disabling VoIP support

* feat(client): allow disabling VoIP support

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>

* add a unit-test for disableVoip option

* fix lint issue

---------

Signed-off-by: Bahaa Naamneh <b.naamneh@gmail.com>
Co-authored-by: Bahaa Naamneh <glimm.no@gmail.com>
2025-10-01 16:52:36 +00:00
Michael Telatynski 52bcc2c955 Update community PR check to exclude dependabot (#5029) 2025-10-01 12:01:33 +00:00
renovate[bot] 1994806c72 Update typescript-eslint monorepo to v8.44.1 (#5025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:31:31 +00:00
renovate[bot] c4d1fd2c67 Update guibranco/github-status-action-v2 digest to 5530c59 (#5023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:18:48 +00:00
renovate[bot] 7ad8288525 Update dependency @types/node to v18.19.127 (#5024)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:08:41 +00:00
renovate[bot] 31a42964e6 Update all non-major dependencies (#5026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:07:57 +00:00
renovate[bot] 2b1d37813c Update dependency @stylistic/eslint-plugin to v5.4.0 (#5027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 14:07:29 +00:00
RiotRobot e6fd0c58ff v38.4.0-rc.0 2025-09-30 12:41:53 +00:00
Will Hunt 41d70d0b5d Add call intent to RTC call notifications (#5010)
* Add media hint specifier

* Refactor to use m.call.intent and to apply to membership

* lint

* Add a mechanism to get the consensus of a call.

* Update tests

* Expose option to update the call intent.

* Better docs

* Add tests

* lint
2025-09-25 09:02:35 +00:00
Skye Elliot a08a2737e1 Implement experimental encrypted state events. (#4994)
* feat: Implement experimental encrypted state events.

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

* fix: Add cast from StateEvents[K] to IContent.

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-09-24 11:44:17 +00:00
Andy Balaam dbe441de33 Exclude cancelled requests from in-progress lists (#5016)
Fixes https://github.com/element-hq/element-web/issues/29882

When we ask for the in-progress verification requests, exclude requests
that have been cancelled. This means that we don't erroneously tell the
user that the new request they are about to create has been cancelled.
2025-09-23 13:51:19 +00:00
RiotRobot 9f3ca71495 Merge branch 'master' into develop 2025-09-23 12:13:04 +00:00
RiotRobot ef97df8ed0 v38.3.0 2025-09-23 12:12:22 +00:00
renovate[bot] 7f74fcc9f7 Update dependency debug to v4.4.3 (#5012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 11:30:33 +00:00
renovate[bot] e39644ad08 Update dependency @stylistic/eslint-plugin to v5.3.1 (#5002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 09:49:25 +00:00
renovate[bot] df0f0074b4 Update dependency uuid to v13 (#5009)
* Update dependency uuid to v13

* Make jest 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-09-16 20:00:04 +00:00
renovate[bot] 29fbed5603 Update dependency p-retry to v7 (#5008)
* Update dependency p-retry to v7

* Iterate

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

* Make jest happier

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-09-16 18:45:28 +00:00
renovate[bot] 5ee6fc196b Update actions/stale action to v10 (#5007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 16:23:39 +00:00
renovate[bot] 961e32a3bb Update actions/setup-node action to v5 (#5006)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 15:30:02 +00:00
renovate[bot] 25f0418fce Update babel monorepo to v7.28.4 (#4998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 14:10:18 +00:00
RiotRobot 0ce751c462 v38.3.0-rc.0 2025-09-16 14:09:05 +00:00
renovate[bot] b7b3588cb8 Update shogo82148/actions-upload-release-asset digest to 59cbc56 (#4996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:58:13 +00:00
renovate[bot] 72846c713d Update actions/github-script action to v8 (#5005)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:56:33 +00:00
renovate[bot] 7c5229b4c8 Update typescript-eslint monorepo to v8.43.0 (#5004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:50:53 +00:00
renovate[bot] dd08388397 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v15.3.0 (#5001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:50:32 +00:00
renovate[bot] ec1ccebcca Update dependency typedoc to v0.28.13 (#5000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:46:32 +00:00
renovate[bot] e9b45cc504 Update dependency @types/node to v18.19.124 (#4999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:46:01 +00:00
renovate[bot] 5d4df65c09 Update all non-major dependencies (#4997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 13:45:36 +00:00
RiotRobot 2706873948 Merge branch 'master' into develop 2025-09-16 11:40:18 +00:00
RiotRobot 4a9006aea6 v38.2.0 2025-09-16 11:39:28 +00:00
Michael Telatynski 43c72d5bf5 Merge commit from fork
* Validate room upgrade relationships in MatrixClient::getJoinedRooms

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

* Tests

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-09-16 12:36:14 +01:00
Andy Balaam e551b92a07 Update matrix-sdk-crypto-wasm to 15.2.0 (#4991)
* Update matrix-sdk-crypto-wasm to 15.2.0

Most relevant changes:

-   History sharing: improve efficiency of building key bundle
    ([matrix-rust-sdk#5513](https://github.com/matrix-org/matrix-rust-sdk/issues/5513))

* Work around matrix-rust-sdk#5643

Modify the message content coming from Rust API to include the missing
property `msgtype: m.key.verification.request`
2025-09-11 14:53:13 +00:00
Richard van der Hoff 32f51e852b History sharing: do /keys/query before checking for key bundle (#4992)
* History sharing: do `/keys/query` before checking for key bundle

The next release of matrix-sdk-crypto-wasm will check that the device that sent
us the key bundle data was correctly cross-signed by its owner, which means we
need to have the owner's cross-signing keys before we check if we have the
bundle.

This replicates a change made in the Rust SDK, at https://github.com/matrix-org/matrix-rust-sdk/pull/5510/files#diff-9f89fa75c4eb3743ae674be1bb90f75169bd815a259917799c71b8a546449d51R133-R140.

* fix unit test

* Comment
2025-09-11 10:38:14 +00:00
RiotRobot 82aa04d894 Merge branch 'master' into develop 2025-09-09 16:30:39 +00:00
RiotRobot ff89c9ec42 v38.1.0 2025-09-09 16:29:55 +00:00
Svajūnas Budrys ccd825fb39 Remove knock state on join (#4977)
Signed-off-by: Svajunas Budrys <svajunas.budrys.sb@gmail.com>
2025-09-04 09:31:26 +00:00
renovate[bot] b313eb5912 Update dependency @types/sdp-transform to v2.15.0 (#4987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 09:06:51 +00:00
renovate[bot] 2b12675675 Update typescript-eslint monorepo to v8.41.0 (#4989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 17:14:06 +00:00
renovate[bot] 246788b874 Update shogo82148/actions-upload-release-asset digest to e6cd457 (#4985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 17:13:57 +00:00
renovate[bot] b0b80401aa Update actions/upload-pages-artifact action to v4 (#4990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 13:39:36 +00:00
renovate[bot] 6a0164f37f Update dependency knip to v5.63.0 (#4988)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 13:30:21 +00:00
renovate[bot] f963d61bcb Update typedoc (#4986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 13:29:33 +00:00
RiotRobot b32619ad24 v38.1.0-rc.0 2025-09-02 12:53:40 +00:00
Tulir Asokan 3d3c3ba55f Fix m.topic format (#4984)
* Fix m.topic format

Fixes #4902

Signed-off-by: Tulir Asokan <tulir@maunium.net>

* Update tests

Signed-off-by: Tulir Asokan <tulir@maunium.net>

* Fix formatting

* Re-add temporary support for invalid form

Signed-off-by: Tulir Asokan <tulir@maunium.net>

---------

Signed-off-by: Tulir Asokan <tulir@maunium.net>
2025-09-02 12:14:08 +00:00
Timo d62c658a72 Remove custom org.matrix.msc4075.rtc.notification.parent relation type (#4979)
* Remove custom `org.matrix.msc4075.rtc.notification.parent` relation type

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

* fix test

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

* Fix test post rebase

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Valere <bill.carson@valrsoft.com>
2025-09-02 08:36:26 +00:00
David Baker bdc4a69023 Fix stable-suffixed MSC4133 support (#4983)
* Fix stable-suffixed MSC4133 support

It looked for the ".stable" suffixed feature to work out what URL to use but not to see whether the server supported it.

This will only be relevant until the next spec release but may as well fix it.

See also https://github.com/element-hq/element-web/pull/30649

* Fix awaiting
2025-09-01 15:34:01 +01:00
Bas Nijholt ab892420b5 Fix thread edit aggregation race condition (#4980)
* test(thread): add regression tests for edit-race; ensure reaction aggregation idempotence

- Edit race: add failing test when `Replace` aggregated pre-init
- Reaction: ensure pre-init aggregation and dedup on replay
- Strengthen assertions for ordering and content

* fix(thread): apply edits after init; keep reactions pre-init; remove redundant aggregation
- Defer Replace aggregation until thread initialised and event is in timeline
- Aggregate Annotation pre-init to preserve reaction summaries
- Rely on EventTimelineSet to aggregate post-insert
- Fixes: element-hq/element-web#30617

* style: run prettier; docs: clarify reaction pre-init comment about counts
2025-08-28 09:03:33 +00:00
RiotRobot ed607c48b0 Merge branch 'master' into develop 2025-08-27 13:32:25 +00:00
RiotRobot a8d75b81e5 v38.0.0 2025-08-27 13:31:35 +00:00
Timo 2f1d654f14 MatrixRTC: Add RTC decline event (#4978)
* Add decline event

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

* add `client.sendRtcDecline`.

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

* remove `decline.reason` field.

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-26 15:21:40 +00:00
Timo c4c7f94514 Make a MatrixRTCSession emit once the RTCNotification is sent (#4976)
* MatrixRTCSession emits once the rtc notification is sent.

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

* update correct type description

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

* Add test

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

* fix imports

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-26 15:16:02 +00:00
David Baker 1fac06e223 Use hydra semantics for unknown room versions (#4957)
This inverts the check for whether to use hydra semantics to only NOT use
it for known, old room versions and use hydra for everything else, so
rooms with versions we don't know about will use hydra semantics.

This will cause any rooms using old/experiental versions unknown to
the js-sdk to break, but will mean that wehn the next room version
comes out, we'll use hydra for it which is, of course, not a given,
but is way more likely than going back to the old semantics.

The mobile Element clients currently hardcode hydra versions (ie.
as it is without this change, but we expect them to make this same
change soon after the hydra release.

We do NOT expect this to land with the hydra release, but target it for
the release after.

Reverts 1e5054a8ff87f83b0875916aa16f435853bf165a from https://github.com/matrix-org/matrix-js-sdk/pull/4937

See https://github.com/element-hq/element-meta/issues/2921 for public
discussion.
2025-08-26 10:49:44 +00:00
RiotRobot 77c118084b v38.0.0-rc.1 2025-08-21 14:16:23 +00:00
ElementRobot 097bfe451a Release tranche of breaking changes (#4963) (#4975)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2025-08-21 15:08:48 +01:00
Michael Telatynski b80d0091d2 Release tranche of breaking changes (#4963)
* Remove deprecated `IJoinRoomOpts.syncRoom` option (#4914)

This option is non-functional, and was deprecated in
https://github.com/matrix-org/matrix-js-sdk/pull/4913. Remove it altogether.

* Remove support for `onlyData != true` (#4939)

* Remove deprecated fields, methods, utilities (#4959)

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2025-08-21 13:24:36 +00:00
Timo 3a33c658bb Expose the StatusChanged event through the RTCSession (#4974)
* Expose the StatusChanged event through the RTCSession

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

* add membershipManagerStatus public get field

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

* add probably left as a getter

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

* add tests for coverage

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-20 16:48:56 +00:00
Timo 81e42b9531 Add probablyLeft event to the MatrixRTCSession (#4962)
* Add probablyLeft emission to the MatrixRTCSession

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

* add docstring

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

* Review: add additional test + fix pending promises issue.

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

* review: `Pick` only a subset of membership manager events

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

* reveiw: update probablyLeft logic to be more straight forward

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

* fix test

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

* make test not wait for 5s

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

* review

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

* fix linter (rebase)

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

* fix import

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-20 12:05:07 +00:00
renovate[bot] 7f7ecd060d Update dependency lint-staged to v16.1.5 (#4969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 15:06:35 +00:00
renovate[bot] 6126ee125a Update typedoc (#4970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-19 14:27:08 +00:00
renovate[bot] 78f718ff82 Update typescript-eslint monorepo to v8.39.1 (#4971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:27:09 +00:00
renovate[bot] c1c1be0c5d Update actions/download-artifact action to v5 (#4973)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:26:16 +00:00
renovate[bot] 1952eaa1ff Update actions/checkout action to v5 (#4972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:26:08 +00:00
renovate[bot] 8851f8b07c Update dependency @types/node to v18.19.123 (#4968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:51 +00:00
renovate[bot] 15eafe34b3 Update babel monorepo to v7.28.3 (#4966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:25 +00:00
renovate[bot] f0d48236fa Update dependency @stylistic/eslint-plugin to v5.2.3 (#4967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:23 +00:00
renovate[bot] dde9d48726 Update shogo82148/actions-upload-release-asset digest to 62365f2 (#4965)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 13:24:06 +00:00
RiotRobot 6d046edcb2 v38.0.0-rc.0 2025-08-19 13:08:13 +00:00
Timo 2abf7ca795 Allow multiple rtc sessions per room (with different sessionDescriptions) (#4945)
* Introduce sessionDescription

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

* Make sessionDescription part of a MatrixRTCSession

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

* Make session manager only menage session for one sessionDescription

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

* make membership manager aware about session (application + id)
Before this was just hardcoded to a call session

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

* update tests

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

* fix doc comments

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

* Make fields private,  improve comments, improve whitespace, don't use deprecated fields

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

* add test for other application end event

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

* rename call -> session

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

* fix tests

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

* remove id check since its already part of `deepCompare(membership.sessionDescription, sessionDescription)`

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

* remove scope related tests. The id should be the only thing that scopes sessions. everything else is application (session type) specific

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

* review

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

* add test for custom sessionDescription

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

* callMembershipsForRoom to default to call

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

* roomSessionForRoom backwards compatible (And deprecate the call specific method)

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2025-08-19 11:58:57 +00:00
Olivier D 2b46579bd8 Add support for login_hint in authorization url generation (#4943)
* add login_hint to authorization url generation

Signed-off-by: olivier <odelcroi@gmail.com>

Signed-off-by: olivier <odelcroi@gmail.com>

fix lint

Signed-off-by: olivier <odelcroi@gmail.com>

* update doc

* fix linter

---------

Signed-off-by: olivier <odelcroi@gmail.com>
Co-authored-by: mcalinghee <mcalinghee.dev@gmail.com>
2025-08-18 15:06:59 +00:00
fkwp 6d42ed338e Only process MatrixRTC sessions associated with calls for callMembershipsForRoom (#4960)
* Only process MatrixRTC sessions associated with calls

* tests: Only process MatrixRTC sessions associated with calls

* linting
2025-08-14 12:44:41 +00:00
RiotRobot ef080c25f9 Merge branch 'master' into develop 2025-08-11 11:13:57 +00:00
David Baker dea184e9ec Support v12 rooms in maySendEvent (#4955)
Follows on from https://github.com/matrix-org/matrix-js-sdk/pull/4937
2025-08-08 13:19:51 +00:00
David Baker e119bf9040 Support for creator power level (#4937)
* Support for creator power level

Adds support for infinite power level specified by [MSC4289](https://github.com/matrix-org/matrix-spec-proposals/pull/4289).

* Update unit test

* Hardcode versions

as room versions strings aren't ordered

* Add test for v12 rooms

* Use more compact syntax

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

* Fix doc

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

* Fix additionalCreators from PR edit

* Split out hydra room version check

* Move power level logic into room state

Which already has knowledge of the room create event

* Add docs

* Fix unused bits

* Fix docs

* Fix lying docstring

* Reverse logic for hydra semantics

Assume unknown room versions do use hydra

* Use backticks

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

* Switch back to hardcoding just the two hydra versions

---------

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-08-08 10:23:49 +00:00
renovate[bot] c7f982e190 Update typedoc (#4949)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 14:25:57 +00:00
renovate[bot] 2e2dd628c1 Update typescript-eslint monorepo to v8.39.0 (#4952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:56:03 +00:00
renovate[bot] 5ac5a8a799 Update dependency @babel/runtime to v7.28.2 (#4950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:55:49 +00:00
renovate[bot] 7d75ab417a Update dependency lint-staged to v16.1.4 (#4948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:40:14 +00:00
renovate[bot] 3ca81e409a Update dependency @types/node to v18.19.121 (#4947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 13:40:03 +00:00
264 changed files with 23021 additions and 14968 deletions
+28 -12
View File
@@ -1,6 +1,6 @@
module.exports = { module.exports = {
plugins: ["matrix-org", "import", "jsdoc", "n"], plugins: ["matrix-org", "import", "jsdoc", "n", "@vitest"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"], extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
parserOptions: { parserOptions: {
project: ["./tsconfig.json"], 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: [ overrides: [
{ {
@@ -147,11 +137,37 @@ module.exports = {
}, },
{ {
files: ["spec/**/*.ts"], files: ["spec/**/*.ts"],
extends: ["plugin:@vitest/legacy-recommended"],
rules: { rules: {
// We don't need super strict typing in test utilities // We don't need super strict typing in test utilities
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off", "@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-empty-object-type": "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 * @matrix-org/element-web-reviewers
/.github/workflows/** @matrix-org/element-web-team /.github/workflows/** @matrix-org/element-web-team
/package.json @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 /scripts/** @matrix-org/element-web-team
/src/webrtc @matrix-org/element-call-reviewers /src/webrtc @matrix-org/element-call-reviewers
/src/matrixrtc @matrix-org/element-call-reviewers /src/matrixrtc @matrix-org/element-call-reviewers
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature - name: Upload tarball signature
if: ${{ inputs.upload-url }} if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1 uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with: with:
upload_url: ${{ inputs.upload-url }} upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures - name: Upload asset signatures
if: inputs.gpg-fingerprint if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1 uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with: with:
upload_url: ${{ inputs.upload-url }} upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets - name: Upload assets
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1 uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with: with:
upload_url: ${{ inputs.upload-url }} upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }} asset_path: ${{ inputs.asset-path }}
+3
View File
@@ -41,3 +41,6 @@
- name: "Z-Flaky-Test" - name: "Z-Flaky-Test"
description: "A test is raising false alarms" description: "A test is raising false alarms"
color: "ededed" 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 name: Backport
on: 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: types:
- closed - closed
- labeled - labeled
+4 -2
View File
@@ -1,7 +1,9 @@
name: Deploy documentation PR preview name: Deploy documentation PR preview
on: 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"] workflows: ["Static Analysis"]
types: types:
- completed - completed
@@ -15,7 +17,7 @@ jobs:
deployments: write deployments: write
steps: steps:
- name: 📥 Download artifact - name: 📥 Download artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}
@@ -1,7 +1,7 @@
# Triggers after the "Downstream artifacts" build has finished, to run the # 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: on:
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
@@ -21,11 +21,12 @@ concurrency:
jobs: jobs:
playwright: playwright:
name: 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: permissions:
actions: read actions: read
issues: read issues: read
pull-requests: read pull-requests: read
contents: read
with: with:
matrix-js-sdk-sha: ${{ github.sha }} matrix-js-sdk-sha: ${{ github.sha }}
# We only want to run the playwright tests on merge queue to prevent regressions # 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 runs-on: ubuntu-24.04
steps: steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it - name: Notify element-web repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3 uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with: with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }} repository: ${{ matrix.repo }}
+11 -6
View File
@@ -1,6 +1,9 @@
name: Pull Request name: Pull Request
on: 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] types: [opened, edited, labeled, unlabeled, synchronize]
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
@@ -15,7 +18,7 @@ jobs:
name: Preview Changelog name: Preview Changelog
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5 - uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
if: github.event_name != 'merge_group' if: github.event_name != 'merge_group'
with: with:
labels: | labels: |
@@ -35,7 +38,7 @@ jobs:
pull-requests: read pull-requests: read
steps: steps:
- name: Add notice - name: Add notice
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked') if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with: with:
script: | script: |
@@ -49,7 +52,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Check membership - name: Check membership
if: github.event.pull_request.user.login != 'renovate[bot]' if: github.event.pull_request.user.login != 'renovate[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3 uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
id: teams id: teams
with: with:
@@ -60,7 +63,7 @@ jobs:
- name: Add label - name: Add label
if: steps.teams.outputs.isTeamMember == 'false' if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with: with:
script: | script: |
github.rest.issues.addLabels({ github.rest.issues.addLabels({
@@ -73,13 +76,15 @@ jobs:
close-if-fork-develop: close-if-fork-develop:
name: Forbid develop branch fork contributions name: Forbid develop branch fork contributions
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
pull-requests: write
if: > if: >
github.event.action == 'opened' && github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' && github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository github.event.pull_request.head.repo.full_name != github.repository
steps: steps:
- name: Close pull request - name: Close pull request
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with: with:
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Check for X-Release-Blocker label on any open issues or PRs - name: Check for X-Release-Blocker label on any open issues or PRs
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env: env:
REPO: ${{ inputs.repository }} REPO: ${{ inputs.repository }}
with: with:
@@ -16,18 +16,20 @@ jobs:
contents: write contents: write
steps: steps:
- name: 🧮 Checkout code - name: 🧮 Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
ref: staging ref: staging
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version-file: package.json node-version-file: package.json
cache: "yarn" cache: "pnpm"
- name: Install Deps - name: Install Deps
run: "yarn install --frozen-lockfile" run: "pnpm install --frozen-lockfile"
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad - uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
id: draft-release id: draft-release
@@ -37,7 +39,7 @@ jobs:
disable-autolabeler: true disable-autolabeler: true
- name: Get actions scripts - name: Get actions scripts
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
repository: matrix-org/matrix-js-sdk repository: matrix-org/matrix-js-sdk
persist-credentials: false persist-credentials: false
@@ -48,7 +50,7 @@ jobs:
- name: Ingest upstream changes - name: Ingest upstream changes
if: inputs.include-changes if: inputs.include-changes
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }} RELEASE_ID: ${{ steps.draft-release.outputs.id }}
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
draft: draft:
permissions: permissions:
contents: write 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. description: List of dependencies to reset.
type: string type: string
required: false required: false
dir:
description: The directory to release
type: string
default: "."
concurrency: ${{ github.workflow }} concurrency: ${{ github.workflow }}
permissions: {} # Uses ELEMENT_BOT_TOKEN permissions: {} # Uses ELEMENT_BOT_TOKEN
jobs: jobs:
merge: merge:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN # 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 }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
persist-credentials: true
- name: Get actions scripts - name: Get actions scripts
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
repository: matrix-org/matrix-js-sdk repository: matrix-org/matrix-js-sdk
persist-credentials: false persist-credentials: false
@@ -33,13 +38,14 @@ jobs:
sparse-checkout: | sparse-checkout: |
scripts/release scripts/release
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "yarn" cache: "pnpm"
node-version-file: package.json node-version-file: package.json
- name: Install Deps - name: Install Deps
run: "yarn install --frozen-lockfile" run: "pnpm install --frozen-lockfile"
- name: Set up git - name: Set up git
run: | run: |
@@ -53,6 +59,7 @@ jobs:
- name: Reset dependencies - name: Reset dependencies
if: inputs.dependencies if: inputs.dependencies
working-directory: ${{ inputs.dir }}
run: | run: |
while IFS= read -r PACKAGE; do while IFS= read -r PACKAGE; do
[ -z "$PACKAGE" ] && continue [ -z "$PACKAGE" ] && continue
@@ -73,7 +80,7 @@ jobs:
fi fi
echo "Resetting $PACKAGE to develop branch..." echo "Resetting $PACKAGE to develop branch..."
yarn add "github:matrix-org/$PACKAGE#develop" pnpm add "github:matrix-org/$PACKAGE#develop"
git add -u git add -u
git commit -m "Reset $PACKAGE back to develop branch" git commit -m "Reset $PACKAGE back to develop branch"
done <<< "$DEPENDENCIES" done <<< "$DEPENDENCIES"
+39 -23
View File
@@ -4,8 +4,6 @@ on:
secrets: secrets:
ELEMENT_BOT_TOKEN: ELEMENT_BOT_TOKEN:
required: true required: true
NPM_TOKEN:
required: false
GPG_PASSPHRASE: GPG_PASSPHRASE:
required: false required: false
GPG_PRIVATE_KEY: GPG_PRIVATE_KEY:
@@ -28,12 +26,21 @@ on:
description: | description: |
The path to the asset you want to upload, if any. You can use glob patterns here. 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. Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
Relative to `dir`.
type: string type: string
required: false required: false
expected-asset-count: expected-asset-count:
description: The number of expected assets, including signatures, excluding generated zip & tarball. description: The number of expected assets, including signatures, excluding generated zip & tarball.
type: number type: number
required: false 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: outputs:
npm-id: npm-id:
description: "The npm package@version string we published" description: "The npm package@version string we published"
@@ -45,7 +52,7 @@ jobs:
permissions: permissions:
issues: read issues: read
pull-requests: 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: release:
name: Release name: Release
@@ -58,7 +65,7 @@ jobs:
- name: Load GPG key - name: Load GPG key
id: gpg id: gpg
if: inputs.gpg-fingerprint if: inputs.gpg-fingerprint
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6 uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }} passphrase: ${{ secrets.GPG_PASSPHRASE }}
@@ -66,22 +73,23 @@ jobs:
- name: Get draft release - name: Get draft release
id: draft-release id: draft-release
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1 uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
with: with:
draft: true draft: true
latest: true latest: true
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
ref: staging 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 # 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 }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
persist-credentials: true
- name: Get actions scripts - name: Get actions scripts
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
repository: matrix-org/matrix-js-sdk repository: matrix-org/matrix-js-sdk
persist-credentials: false persist-credentials: false
@@ -92,6 +100,7 @@ jobs:
- name: Prepare variables - name: Prepare variables
id: prepare id: prepare
working-directory: ${{ inputs.dist-dir }}
run: | run: |
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
@@ -106,7 +115,7 @@ jobs:
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
- name: Check version number not in use - name: Check version number not in use
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with: with:
script: | script: |
const { VERSION } = process.env; const { VERSION } = process.env;
@@ -125,15 +134,17 @@ jobs:
git config --global user.email "releases@riot.im" git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot" git config --global user.name "RiotRobot"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "yarn" cache: "pnpm"
node-version-file: package.json node-version-file: ${{ inputs.dist-dir }}/package.json
- name: Install dependencies - name: Install dependencies
run: "yarn install --frozen-lockfile" run: "pnpm install --frozen-lockfile"
- name: Handle develop dependencies - name: Handle develop dependencies
working-directory: ${{ inputs.dist-dir }}
run: | run: |
ret=0 ret=0
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
@@ -142,15 +153,19 @@ jobs:
VERSION=${dep[1]} VERSION=${dep[1]}
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION" 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 add -u
git commit -m "Keep $PACKAGE at $VERSION" git commit -m "Keep $PACKAGE at $VERSION"
done done
- name: Bump package.json version - name: Bump package.json versions
run: | run: |
yarn version --no-git-tag-version --new-version "${VERSION#v}" for DIR in $DIRS; do
git add package.json 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 - name: Add to CHANGELOG.md
if: inputs.final if: inputs.final
@@ -177,7 +192,8 @@ jobs:
- name: Build assets - name: Build assets
if: steps.prepare.outputs.has-dist-script == '1' 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 - name: Upload release assets & signatures
if: inputs.asset-path if: inputs.asset-path
@@ -185,7 +201,7 @@ jobs:
with: with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }} gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.draft-release.outputs.upload_url }} 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 - name: Create signed tag
if: inputs.gpg-fingerprint if: inputs.gpg-fingerprint
@@ -218,7 +234,7 @@ jobs:
- name: Validate release has expected assets - name: Validate release has expected assets
if: inputs.expected-asset-count if: inputs.expected-asset-count
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env: env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }} RELEASE_ID: ${{ steps.draft-release.outputs.id }}
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }} EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
@@ -246,7 +262,7 @@ jobs:
git push origin master git push origin master
- name: Publish release - name: Publish release
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env: env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }} RELEASE_ID: ${{ steps.draft-release.outputs.id }}
FINAL: ${{ inputs.final }} FINAL: ${{ inputs.final }}
@@ -278,12 +294,12 @@ jobs:
name: Publish to npm name: Publish to npm
needs: release needs: release
if: inputs.npm 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: permissions:
contents: read contents: read
id-token: write id-token: write
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
post-release: post-release:
name: Post release steps name: Post release steps
+20 -18
View File
@@ -1,9 +1,11 @@
name: Publish to npm name: Publish to npm
on: on:
workflow_call: workflow_call:
secrets: inputs:
NPM_TOKEN: dir:
required: true description: The directory to release
type: string
default: "."
outputs: outputs:
id: id:
description: "The npm package@version string we published" description: "The npm package@version string we published"
@@ -20,32 +22,32 @@ jobs:
id: ${{ steps.npm-publish.outputs.id }} id: ${{ steps.npm-publish.outputs.id }}
steps: steps:
- name: 🧮 Checkout code - name: 🧮 Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
ref: staging ref: staging
persist-credentials: false
- name: 🔧 Yarn cache - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - name: 🔧 pnpm cache
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "yarn" cache: "pnpm"
registry-url: "https://registry.npmjs.org" 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 - name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile" run: "pnpm install --frozen-lockfile"
- name: 🚀 Publish to npm - name: 🚀 Publish to npm
id: npm-publish id: npm-publish
working-directory: ${{ inputs.dir }}
run: | run: |
npm publish --provenance --access public --tag next npm publish --provenance --access public --tag "$TAG"
release=$(jq -r '"\(.name)@\(.version)"' package.json) release=$(jq -r '"\(.name)@\(.version)"' package.json)
echo "id=$release" >> $GITHUB_OUTPUT echo "id=$release" >> $GITHUB_OUTPUT
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} TAG: ${{ contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest' }}
- name: 🎖️ Add `latest` dist-tag to final releases
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
run: npm dist-tag add "$release" latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.id }}
+24 -17
View File
@@ -24,7 +24,7 @@ concurrency: ${{ github.workflow }}
permissions: {} # No permissions required permissions: {} # No permissions required
jobs: jobs:
release: 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: permissions:
contents: write contents: write
issues: write issues: write
@@ -41,28 +41,32 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
strategy: strategy:
matrix: matrix:
repo: include:
- element-hq/element-web - repo: element-hq/element-web
path: apps/web
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
repository: ${{ matrix.repo }} repository: ${{ matrix.repo }}
ref: staging ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
persist-credentials: true
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "yarn" cache: "pnpm"
node-version: "lts/*" node-version: "lts/*"
- name: Bump dependency - name: Bump dependency
env: env:
DEPENDENCY: ${{ needs.release.outputs.npm-id }} DEPENDENCY: ${{ needs.release.outputs.npm-id }}
DIR: ${{ matrix.path }}
run: | run: |
git config --global user.email "releases@riot.im" git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot" git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact pnpm add -C "$DIR" "$DEPENDENCY" --save-exact
git add package.json yarn.lock git add "$DIR"/package.json pnpm-lock.yaml
git commit -am"Upgrade dependency to $DEPENDENCY" git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging git push origin staging
@@ -73,22 +77,25 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: 🧮 Checkout code - name: 🧮 Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: 🔧 Yarn cache
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: 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 node-version-file: package.json
- name: 🔨 Install dependencies - name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile" run: "pnpm install --frozen-lockfile"
- name: 📖 Generate docs - name: 📖 Generate docs
run: yarn gendoc run: pnpm gendoc
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with: with:
path: _docs path: _docs
@@ -106,4 +113,4 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
+14 -9
View File
@@ -12,7 +12,11 @@ on:
sharded: sharded:
type: boolean type: boolean
required: false 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: {} permissions: {}
jobs: jobs:
sonarqube: sonarqube:
@@ -27,7 +31,7 @@ jobs:
steps: steps:
# We create the status here and then update it to success/failure in the `report` stage # 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. # This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
- uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685 - uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending state: pending
@@ -36,14 +40,15 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code" - name: "🧮 Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
repository: ${{ github.event.workflow_run.head_repository.full_name }} repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow 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 fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
persist-credentials: false
- name: 📥 Download artifact - name: 📥 Download artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: ${{ !inputs.sharded }} if: ${{ !inputs.sharded }}
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -51,14 +56,13 @@ jobs:
name: coverage name: coverage
path: coverage path: coverage
- name: 📥 Download sharded artifacts - name: 📥 Download sharded artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: inputs.sharded if: inputs.sharded
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }} run-id: ${{ github.event.workflow_run.id }}
pattern: coverage-* pattern: coverage-*
path: coverage path: coverage
merge-multiple: true
- name: Check coverage artifact - name: Check coverage artifact
run: | run: |
if [ ! -d coverage ]; then if [ ! -d coverage ]; then
@@ -75,19 +79,20 @@ jobs:
- name: "🩻 SonarCloud Scan" - name: "🩻 SonarCloud Scan"
id: sonarcloud 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 # 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' }} continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with: with:
skip_checkout: true skip_checkout: true
repository: ${{ github.event.workflow_run.head_repository.full_name }} repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }} 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 }} branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }} revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }} token: ${{ secrets.SONAR_TOKEN }}
- uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685 - uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
if: always() if: always()
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
+4 -2
View File
@@ -1,6 +1,8 @@
name: SonarQube name: SonarQube
on: 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"] workflows: ["Tests"]
types: types:
- completed - completed
@@ -16,7 +18,7 @@ jobs:
actions: read actions: read
statuses: write statuses: write
id-token: write # sonar 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: secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+79 -53
View File
@@ -14,58 +14,61 @@ jobs:
name: "Typescript Syntax Check" name: "Typescript Syntax Check"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: 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 node-version-file: package.json
- name: Install Deps - name: Install Deps
run: "yarn install" run: "pnpm install"
- name: Typecheck - name: Typecheck
run: "yarn run lint:types" run: "pnpm run lint:types"
js_lint: js_lint:
name: "ESLint" name: "ESLint"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: 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 node-version-file: package.json
- name: Install Deps - name: Install Deps
run: "yarn install" run: "pnpm install"
- name: Run Linter - name: Run Linter
run: "yarn run lint:js" run: "pnpm run lint:js"
node_example_lint: node_example_lint:
name: "Node.js example" name: "Node.js example"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: 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 node-version-file: package.json
- name: Install Deps - name: Install Deps
run: "yarn install" run: "pnpm install"
- name: Build Types - name: Build Types
run: "yarn build:types" run: "pnpm build:types"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "npm"
node-version-file: "examples/node/package.json"
# cache-dependency-path: '**/package-lock.json'
- name: Install Example Deps - name: Install Example Deps
run: "npm install" run: "npm install"
@@ -82,39 +85,50 @@ jobs:
workflow_lint: workflow_lint:
name: "Workflow Lint" name: "Workflow Lint"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
security-events: write
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: 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 node-version-file: package.json
- name: Install Deps - name: Install Deps
run: "yarn install --frozen-lockfile" run: "pnpm install --frozen-lockfile"
- name: Run Linter - name: Run Linter
run: "yarn lint:workflows" run: "pnpm lint:workflows"
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
docs: docs:
name: "JSDoc Checker" name: "JSDoc Checker"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: 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 node-version-file: package.json
- name: Install Deps - name: Install Deps
run: "yarn install" run: "pnpm install"
- name: Generate Docs - name: Generate Docs
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles" run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
name: docs name: docs
path: _docs path: _docs
@@ -125,31 +139,36 @@ jobs:
name: "Analyse Dead Code" name: "Analyse Dead Code"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: 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 node-version-file: package.json
- name: Install Deps - name: Install Deps
run: "yarn install --frozen-lockfile" run: "pnpm install --frozen-lockfile"
- name: Run linter - name: Run linter
run: "yarn run lint:knip" run: "pnpm run lint:knip"
element-web: element-web:
name: Downstream tsc element-web name: Downstream tsc element-web
if: github.event_name == 'merge_group' if: github.event_name == 'merge_group'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
repository: element-hq/element-web repository: element-hq/element-web
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "yarn" cache: "pnpm"
node-version: "lts/*" node-version: "lts/*"
- name: Install Dependencies - name: Install Dependencies
@@ -159,15 +178,22 @@ jobs:
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }} JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
- name: Typecheck - 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 # Workflow consolidation job
downstream: done:
name: Downstream Typescript Syntax Check needs:
- ts_lint
- js_lint
- node_example_lint
- workflow_lint
- docs
- analyse_dead_code
- element-web
name: Static Analysis
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
if: always() if: always()
needs:
- element-web
steps: steps:
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success' - if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1 run: exit 1
+1 -1
View File
@@ -11,7 +11,7 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
sync-labels: 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: with:
LABELS: | LABELS: |
element-hq/element-meta element-hq/element-meta
+26 -24
View File
@@ -12,8 +12,8 @@ env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }} ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
permissions: {} # No permissions required permissions: {} # No permissions required
jobs: jobs:
jest: test:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})" name: "Vitest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
timeout-minutes: 10 timeout-minutes: 10
strategy: strategy:
@@ -22,17 +22,20 @@ jobs:
node: ["lts/*", 22] node: ["lts/*", 22]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node - name: Setup Node
id: setupNode id: setupNode
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
cache: "yarn" cache: "pnpm"
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- name: Install dependencies - name: Install dependencies
run: "yarn install" run: "pnpm install"
- name: Get number of CPU cores - name: Get number of CPU cores
id: cpu-cores id: cpu-cores
@@ -40,24 +43,23 @@ jobs:
- name: Run tests - name: Run tests
run: | run: |
yarn test \ pnpm test \
--coverage=${{ env.ENABLE_COVERAGE }} \ --coverage=${ENABLE_COVERAGE} \
--ci \ --maxWorkers ${NUM_WORKERS} \
--max-workers ${{ steps.cpu-cores.outputs.count }} \
./spec/${{ matrix.specs }} ./spec/${{ matrix.specs }}
env: env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true SHARD: ${{ matrix.specs }}
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
# tell jest to use coloured output
FORCE_COLOR: true
- name: Move coverage files into place - name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true' 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 - name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true' if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }} name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
path: | path: |
@@ -65,19 +67,19 @@ jobs:
!coverage/lcov-report !coverage/lcov-report
# Dummy completion job to simplify branch protections # Dummy completion job to simplify branch protections
jest-complete: complete:
name: Jest tests name: Tests
needs: jest needs: test
if: always() if: always()
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success' - if: needs.test.result != 'skipped' && needs.test.result != 'success'
run: exit 1 run: exit 1
element-web: element-web:
name: Downstream test element-web name: Downstream test element-web
if: github.event_name == 'merge_group' 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: permissions:
statuses: write statuses: write
with: with:
@@ -87,8 +89,8 @@ jobs:
complement-crypto: complement-crypto:
name: "Run Complement Crypto tests" name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group' if: github.event_name == 'merge_group'
permissions: read-all permissions: read-all # zizmor: ignore[excessive-permissions]
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
with: with:
use_js_sdk: "." use_js_sdk: "."
@@ -116,7 +118,7 @@ jobs:
steps: steps:
- name: Skip SonarCloud on merge queues - name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false' if: env.ENABLE_COVERAGE == 'false'
uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685 uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: success state: success
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
automate-project-columns-next: automate-project-columns-next:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/add-to-project@main - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with: with:
project-url: https://github.com/orgs/element-hq/projects/120 project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+1 -1
View File
@@ -6,6 +6,6 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs: jobs:
call-triage-labelled: 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: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
issues: write issues: write
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
with: with:
operations-per-run: 250 operations-per-run: 250
days-before-issue-stale: -1 days-before-issue-stale: -1
+1
View File
@@ -3,6 +3,7 @@
/.npmrc /.npmrc
/*.log /*.log
pnpm-lock.yaml
package-lock.json package-lock.json
.lock-wscript .lock-wscript
build/Release build/Release
+249
View File
@@ -1,3 +1,252 @@
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
* Re-add truthy check on room name/avatar/alias events ([#5081](https://github.com/matrix-org/matrix-js-sdk/pull/5081)). Contributed by @t3chguy.
* Fix invalid state events corrupting room objects ([#5078](https://github.com/matrix-org/matrix-js-sdk/pull/5078)). Contributed by @t3chguy.
Changes in [39.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.2.0) (2025-11-18)
==================================================================================================
## ✨ Features
* Delayed event management: split endpoints, no auth ([#5066](https://github.com/matrix-org/matrix-js-sdk/pull/5066)). Contributed by @AndrewFerr.
* do not set cache in authenticated fetch ([#5020](https://github.com/matrix-org/matrix-js-sdk/pull/5020)). Contributed by @pkuzco.
## 🐛 Bug Fixes
* Fix media switching during legacy calls ([#5069](https://github.com/matrix-org/matrix-js-sdk/pull/5069)). Contributed by @langleyd.
Changes in [39.1.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.2) (2025-11-04)
==================================================================================================
Re-release of v39.1.0 to fix npm publishing workflow
Changes in [39.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.1) (2025-11-04)
==================================================================================================
Re-release of v39.1.0 to fix npm publishing workflow
Changes in [39.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.0) (2025-11-04)
==================================================================================================
## ✨ Features
* [MatrixRTC] Sticky Events support (MSC4354) ([#5017](https://github.com/matrix-org/matrix-js-sdk/pull/5017)). Contributed by @toger5.
* Add `CryptoApi.getSecretStorageStatus` ([#5054](https://github.com/matrix-org/matrix-js-sdk/pull/5054)). Contributed by @richvdh.
* Add parseCallNotificationContent ([#5015](https://github.com/matrix-org/matrix-js-sdk/pull/5015)). Contributed by @toger5.
* MSC4140: support filters on delayed event lookup ([#5038](https://github.com/matrix-org/matrix-js-sdk/pull/5038)). Contributed by @AndrewFerr.
Changes in [39.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.0.0) (2025-10-21)
==================================================================================================
## 🚨 BREAKING CHANGES
* [MatrixRTC] Multi SFU support + m.rtc.member event type support ([#5022](https://github.com/matrix-org/matrix-js-sdk/pull/5022)). Contributed by @toger5.
## ✨ Features
* [MatrixRTC] Multi SFU support + m.rtc.member event type support ([#5022](https://github.com/matrix-org/matrix-js-sdk/pull/5022)). Contributed by @toger5.
* Implement Sticky Events MSC4354 ([#5028](https://github.com/matrix-org/matrix-js-sdk/pull/5028)). Contributed by @Half-Shot.
* feat(client): allow disabling VoIP support ([#5021](https://github.com/matrix-org/matrix-js-sdk/pull/5021)). Contributed by @pkuzco.
## 🐛 Bug Fixes
* Only use the first 3 viaServers specified ([#5034](https://github.com/matrix-org/matrix-js-sdk/pull/5034)). Contributed by @t3chguy.
* Fetch the user's device info before processing a verification request ([#5030](https://github.com/matrix-org/matrix-js-sdk/pull/5030)). Contributed by @andybalaam.
Changes in [38.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.4.0) (2025-10-07)
==================================================================================================
## ✨ Features
* Add call intent to RTC call notifications ([#5010](https://github.com/matrix-org/matrix-js-sdk/pull/5010)). Contributed by @Half-Shot.
* Implement experimental encrypted state events. ([#4994](https://github.com/matrix-org/matrix-js-sdk/pull/4994)). Contributed by @kaylendog.
## 🐛 Bug Fixes
* Exclude cancelled requests from in-progress lists ([#5016](https://github.com/matrix-org/matrix-js-sdk/pull/5016)). Contributed by @andybalaam.
Changes in [38.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.3.0) (2025-09-23)
==================================================================================================
## 🐛 Bug Fixes
* Remove knocked room when membership changes to join ([#4977](https://github.com/matrix-org/matrix-js-sdk/pull/4977)). Contributed by @svajunas-budrys.
Changes in [38.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.2.0) (2025-09-16)
==================================================================================================
Fix [CVE-2025-59160](https://www.cve.org/CVERecord?id=CVE-2025-59160) / [GHSA-mp7c-m3rh-r56v](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-mp7c-m3rh-r56v)
Changes in [38.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.1.0) (2025-09-09)
==================================================================================================
## ✨ Features
* Remove custom `org.matrix.msc4075.rtc.notification.parent` relation type ([#4979](https://github.com/matrix-org/matrix-js-sdk/pull/4979)). Contributed by @toger5.
* MatrixRTC: Add RTC decline event ([#4978](https://github.com/matrix-org/matrix-js-sdk/pull/4978)). Contributed by @toger5.
* Make a MatrixRTCSession emit once the RTCNotification is sent ([#4976](https://github.com/matrix-org/matrix-js-sdk/pull/4976)). Contributed by @toger5.
* Use hydra semantics for unknown room versions ([#4957](https://github.com/matrix-org/matrix-js-sdk/pull/4957)). Contributed by @dbkr.
* Expose the StatusChanged event through the RTCSession ([#4974](https://github.com/matrix-org/matrix-js-sdk/pull/4974)). Contributed by @toger5.
* Add `probablyLeft` event to the MatrixRTCSession ([#4962](https://github.com/matrix-org/matrix-js-sdk/pull/4962)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix `m.topic` format ([#4984](https://github.com/matrix-org/matrix-js-sdk/pull/4984)). Contributed by @tulir.
* Fix stable-suffixed MSC4133 support ([#4983](https://github.com/matrix-org/matrix-js-sdk/pull/4983)). Contributed by @dbkr.
* Fix thread edit aggregation race condition ([#4980](https://github.com/matrix-org/matrix-js-sdk/pull/4980)). Contributed by @basnijholt.
Changes in [38.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.0.0) (2025-08-27)
==================================================================================================
## 🚨 BREAKING CHANGES
* Release tranche of breaking changes ([#4975](https://github.com/matrix-org/matrix-js-sdk/pull/4975)).
* Remove support for FetchHttpApi `onlyData = false`
* Remove deprecated `IJoinRoomOpts.syncRoom`
* Remove deprecated methods which are unsupported in rust crypto
* Remove deprecated getAuthIssuer method
* Remove deprecated beginKeyVerification method
* Remove deprecated isEncryptedDisabledForUnverifiedDevices getter
* Remove deprecated UndecryptableToDeviceEvent MatrixClient emit
* Remove deprecated defer utility method
* Remove deprecated UIAResponse dummy type
* Remove deprecated MatrixRTCSession MembershipConfig fields
* Remove deprecated findVerificationRequestDMInProgress and storeSessionBackupPrivateKey methods in favour of overloads
## ✨ Features
* Allow multiple rtc sessions per room (with different sessionDescriptions) ([#4945](https://github.com/matrix-org/matrix-js-sdk/pull/4945)). Contributed by @toger5.
* Add support for login\_hint in authorization url generation ([#4943](https://github.com/matrix-org/matrix-js-sdk/pull/4943)). Contributed by @odelcroi.
* Only process MatrixRTC sessions associated with calls for `callMembershipsForRoom` ([#4960](https://github.com/matrix-org/matrix-js-sdk/pull/4960)). Contributed by @fkwp.
Changes in [37.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.13.0) (2025-08-11) Changes in [37.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.13.0) (2025-08-11)
==================================================================================================== ====================================================================================================
This release supports new v12 Matrix rooms and consequently has a breaking change, removing powerLevelNorm from the RoomMember object as this can't be supported with infinite power levels. Apps should use the non-normalised `powerLevel` instead. This release supports new v12 Matrix rooms and consequently has a breaking change, removing powerLevelNorm from the RoomMember object as this can't be supported with infinite power levels. Apps should use the non-normalised `powerLevel` instead.
+1 -1
View File
@@ -117,7 +117,7 @@ checks, so please check back after a few minutes.
Your PR should include tests. Your PR should include tests.
For new user facing features in `matrix-js-sdk`, you 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` 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 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 > 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. > [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) Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
if you do not have it already. 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 ```javascript
import * as sdk from "matrix-js-sdk"; 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: host the API reference from the source files like this:
``` ```
$ yarn gendoc $ pnpm gendoc
$ cd docs $ cd docs
$ python -m http.server 8005 $ 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: First, you need to pull in the right build tools:
``` ```
$ yarn install $ pnpm install
``` ```
## Building ## Building
@@ -461,17 +461,17 @@ First, you need to pull in the right build tools:
To build a browser version from scratch when developing: 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: To run linting:
``` ```
$ yarn lint $ pnpm lint
``` ```
+2 -10
View File
@@ -7,21 +7,13 @@ module.exports = {
targets: { targets: {
esmodules: true, esmodules: true,
}, },
// We want to output ES modules for the final build (mostly to ensure that modules: false,
// 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,
}, },
], ],
[ [
"@babel/preset-typescript", "@babel/preset-typescript",
{ {
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import` rewriteImportExtensions: true,
// 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",
}, },
], ],
], ],
+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. 11. If a variable is not receiving a value on declaration, its type must be defined.
```typescript ```typescript
let errorMessage: Optional<string>; let errorMessage: string;
``` ```
12. Objects can use shorthand declarations, including mixing of types. 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. 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. 27. `import` should be used instead of `require`, as `require` does not have types.
28. Export only what can be reused. 28. Export only what can be reused.
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead 29. Prefer a type like `X | null` instead of truly optional parameters.
of truly optional parameters.
1. A notable exception is when the likelihood of a bug is minimal, such as when a function 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 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 `?` 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 ```typescript
function doThingWithRoom( function doThingWithRoom(
thing: string, 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 ## Tests
1. Tests must be written in TypeScript. 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: 3. Use the following convention template:
```typescript ```typescript
+1 -1
View File
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks # now run our checks
cd "$tmpdir" cd "$tmpdir"
yarn lint pnpm lint
-47
View File
@@ -1,47 +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",
// 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 -10
View File
@@ -1,5 +1,8 @@
import { KnipConfig } from "knip"; 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 { export default {
entry: [ entry: [
"src/index.ts", "src/index.ts",
@@ -12,10 +15,6 @@ export default {
"src/utils.ts", // not really an entrypoint but we have deprecated `defer` there "src/utils.ts", // not really an entrypoint but we have deprecated `defer` there
"scripts/**", "scripts/**",
"spec/**", "spec/**",
// XXX: these look entirely unused
"src/crypto/aes.ts",
"src/crypto/crypto.ts",
"src/crypto/recoverykey.ts",
// XXX: these should be re-exported by one of the supported exports // XXX: these should be re-exported by one of the supported exports
"src/matrixrtc/index.ts", "src/matrixrtc/index.ts",
"src/sliding-sync.ts", "src/sliding-sync.ts",
@@ -32,12 +31,6 @@ export default {
"husky", "husky",
// Used in script which only runs in environment with `@octokit/rest` installed // Used in script which only runs in environment with `@octokit/rest` installed
"@octokit/rest", "@octokit/rest",
// Used by jest
"jest-environment-jsdom",
"babel-jest",
"ts-node",
// Used by `@babel/plugin-transform-runtime`
"@babel/runtime",
], ],
ignoreBinaries: [ ignoreBinaries: [
// Used when available by reusable workflow `.github/workflows/release-make.yml` // Used when available by reusable workflow `.github/workflows/release-make.yml`
+47 -41
View File
@@ -1,27 +1,26 @@
{ {
"name": "matrix-js-sdk", "name": "matrix-js-sdk",
"version": "37.13.0", "version": "41.3.0",
"description": "Matrix Client-Server SDK for Javascript", "description": "Matrix Client-Server SDK for Javascript",
"engines": { "engines": {
"node": ">=22.0.0" "node": ">=22.0.0"
}, },
"scripts": { "scripts": {
"prepare": "yarn build", "prepare": "pnpm build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel --delete-dir-on-start src -w -s -d lib --verbose --extensions \".ts,.js\"",
"clean": "rimraf lib", "build": "pnpm build:compile && pnpm build:types",
"build": "yarn clean && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly", "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", "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": "eslint --max-warnings 0 src spec && prettier --check .",
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec", "lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit", "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:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip", "lint:knip": "knip",
"test": "jest", "test": "vitest run",
"test:watch": "jest --watch", "test:watch": "vitest watch",
"coverage": "yarn test --coverage" "coverage": "pnpm test --coverage"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -49,19 +48,18 @@
], ],
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^15.1.0", "@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
"another-json": "^0.2.0", "another-json": "^0.2.0",
"bs58": "^6.0.0", "bs58": "^6.0.0",
"content-type": "^1.0.4", "content-type": "^1.0.4",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"matrix-events-sdk": "0.0.1", "matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.10.0", "matrix-widget-api": "^1.16.1",
"oidc-client-ts": "^3.0.1", "oidc-client-ts": "^3.0.1",
"p-retry": "4", "p-retry": "8",
"sdp-transform": "^2.14.1", "sdp-transform": "^3.0.0",
"unhomoglyph": "^1.0.6", "unhomoglyph": "^1.0.6"
"uuid": "11"
}, },
"devDependencies": { "devDependencies": {
"@action-validator/cli": "^0.6.0", "@action-validator/cli": "^0.6.0",
@@ -78,18 +76,19 @@
"@babel/plugin-transform-runtime": "^7.12.10", "@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7", "@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", "@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5", "@peculiar/webcrypto": "^1.4.5",
"@stylistic/eslint-plugin": "^5.0.0", "@stylistic/eslint-plugin": "^5.0.0",
"@types/content-type": "^1.1.5", "@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"@types/jest": "^29.0.0", "@types/node": "22",
"@types/node": "18",
"@types/sdp-transform": "^2.4.5", "@types/sdp-transform": "^2.4.5",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
"babel-jest": "^29.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", "babel-plugin-search-and-replace": "^1.1.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"eslint": "8.57.1", "eslint": "8.57.1",
@@ -97,36 +96,43 @@
"eslint-config-prettier": "^10.0.0", "eslint-config-prettier": "^10.0.0",
"eslint-import-resolver-typescript": "^4.0.0", "eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^28.0.0", "eslint-plugin-jsdoc": "^62.0.0",
"eslint-plugin-jsdoc": "^50.0.0", "eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-matrix-org": "2.1.0",
"eslint-plugin-n": "^14.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", "eslint-plugin-unicorn": "^56.0.0",
"fake-indexeddb": "^5.0.2", "fake-indexeddb": "^5.0.2",
"fetch-mock": "11.1.5", "fetch-mock": "^12.6.0",
"fetch-mock-jest": "^1.5.1", "happy-dom": "^20.1.0",
"husky": "^9.0.0", "husky": "^9.0.0",
"jest": "^29.0.0", "knip": "^6.0.0",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"knip": "^5.0.0",
"lint-staged": "^16.0.0", "lint-staged": "^16.0.0",
"matrix-mock-request": "^2.5.0", "matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0", "prettier": "3.8.3",
"prettier": "3.6.2",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"typedoc": "^0.28.1", "typedoc": "^0.28.1",
"typedoc-plugin-coverage": "^4.0.0", "typedoc-plugin-coverage": "^4.0.0",
"typedoc-plugin-mdn-links": "^5.0.0", "typedoc-plugin-mdn-links": "^5.0.0",
"typedoc-plugin-missing-exports": "^4.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": { "pnpm": {
"outputDirectory": "coverage", "peerDependencyRules": {
"outputName": "jest-sonar-report.xml", "allowedVersions": {
"relativePaths": true "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.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=spec/**/* sonar.coverage.exclusions=spec/**/*
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml sonar.testExecutionReportPaths=coverage/sonar-report.xml
sonar.lang.patterns.ts=**/*.ts,**/*.tsx 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 // `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"; import MockHttpBackend from "matrix-mock-request";
@@ -39,7 +39,7 @@ import { type ISyncResponder } from "./test-utils/SyncResponder";
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient * 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. * @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. * use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
*/ */
export class TestClient implements IE2EKeyReceiver, ISyncResponder { 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. limitations under the License.
*/ */
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb"; import { IDBFactory } from "fake-indexeddb";
import debug from "debug"; import debug from "debug";
@@ -74,7 +74,7 @@ describe("cross-signing", () => {
function createCryptoCallbacks(): CryptoCallbacks { function createCryptoCallbacks(): CryptoCallbacks {
return { return {
getSecretStorageKey: (keys, name) => { 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 () => { async () => {
// anything that we don't have a specific matcher for silently returns a 404 // anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404); fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com"; const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({ aliceClient = createClient({
@@ -113,8 +112,7 @@ describe("cross-signing", () => {
); );
afterEach(async () => { afterEach(async () => {
await aliceClient.stopClient(); aliceClient.stopClient();
fetchMock.mockReset();
}); });
/** /**
@@ -138,27 +136,25 @@ describe("cross-signing", () => {
await bootstrapCrossSigning(authDict); await bootstrapCrossSigning(authDict);
// check that the cross-signing keys have been uploaded // check that the cross-signing keys have been uploaded
expect(fetchMock.called("upload-cross-signing-keys")).toBeTruthy(); expect(fetchMock.callHistory.called("upload-cross-signing-keys")).toBeTruthy();
const [, keysOpts] = fetchMock.lastCall("upload-cross-signing-keys")!; const keysOpts = fetchMock.callHistory.lastCall("upload-cross-signing-keys")!.options;
const keysBody = JSON.parse(keysOpts!.body as string); const keysBody = JSON.parse(keysOpts!.body as string);
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
// there should be a key of each type // there should be a key of each type
// master key is signed by the device // 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]; const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
// ssk and usk are signed by the master key // ssk and usk are signed by the master key
expect(keysBody).toHaveProperty(`self_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}]`); expect(keysBody).toHaveProperty(["user_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
const sskId = Object.keys(keysBody.self_signing_key.keys)[0]; const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
// check the publish call // check the publish call
expect(fetchMock.called("upload-sigs")).toBeTruthy(); expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!; const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
const body = JSON.parse(sigsOpts!.body as string); const body = JSON.parse(sigsOpts!.body as string);
// there should be a signature for our device, by our self-signing key. // there should be a signature for our device, by our self-signing key.
expect(body).toHaveProperty( expect(body).toHaveProperty([TEST_USER_ID, TEST_DEVICE_ID, "signatures", TEST_USER_ID, sskId]);
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
);
}); });
it("get cross signing keys from secret storage and import them", async () => { 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(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
// Expect the signature to be uploaded // Expect the signature to be uploaded
expect(fetchMock.called("upload-sigs")).toBeTruthy(); expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!; const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
const body = JSON.parse(sigsOpts!.body as string); const body = JSON.parse(sigsOpts!.body as string);
// the device should have a signature with the public self cross signing keys. // the device should have a signature with the public self cross signing keys.
expect(body).toHaveProperty( expect(body).toHaveProperty([
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`, TEST_USER_ID,
); TEST_DEVICE_ID,
"signatures",
TEST_USER_ID,
`ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}`,
]);
}); });
it("can bootstrapCrossSigning twice", async () => { it("can bootstrapCrossSigning twice", async () => {
@@ -255,8 +255,7 @@ describe("cross-signing", () => {
// a second call should do nothing except GET requests // a second call should do nothing except GET requests
fetchMock.mockClear(); fetchMock.mockClear();
await bootstrapCrossSigning(authDict); await bootstrapCrossSigning(authDict);
const calls = fetchMock.calls((url, opts) => opts.method != "GET"); expect(fetchMock).toHaveFetchedTimes(0, "unmatched");
expect(calls.length).toEqual(0);
}); });
it("will upload existing cross-signing keys to an established secret storage", async () => { 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 // 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. // a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
mockSetupCrossSigningRequests();
const accountDataAccumulator = new AccountDataAccumulator(syncResponder); const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
accountDataAccumulator.interceptGetAccountData(); accountDataAccumulator.interceptGetAccountData();
@@ -282,7 +280,7 @@ describe("cross-signing", () => {
}); });
// Prepare for the cross-signing keys // 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 bootstrapCrossSigning(authDict);
await p; await p;
@@ -403,7 +401,7 @@ describe("cross-signing", () => {
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady(); const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeFalsy(); expect(isCrossSigningReady).toBeFalsy();
}); }, 10000);
}); });
describe("getCrossSigningKeyId", () => { describe("getCrossSigningKeyId", () => {
@@ -415,22 +413,13 @@ describe("cross-signing", () => {
*/ */
function awaitCrossSigningKeysUpload() { function awaitCrossSigningKeysUpload() {
return new Promise<any>((resolve) => { return new Promise<any>((resolve) => {
fetchMock.post( fetchMock.modifyRoute("upload-cross-signing-keys", {
{ response: (callLog) => {
url: new URL( const content = JSON.parse(callLog.options.body as string);
"/_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);
resolve(content); resolve(content);
return {}; return {};
}, },
// Override the route defined in E2EKeyReceiver });
{ overwriteRoutes: true },
);
}); });
} }
@@ -461,9 +450,6 @@ describe("cross-signing", () => {
describe("crossSignDevice", () => { describe("crossSignDevice", () => {
beforeEach(async () => { 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 // make sure that there is another device which we can sign
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA); e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
@@ -477,10 +463,6 @@ describe("cross-signing", () => {
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy(); expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
}); });
afterEach(async () => {
jest.useRealTimers();
});
it("fails for an unknown device", async () => { it("fails for an unknown device", async () => {
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device"); 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); await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
// check that a sig for the device was uploaded // 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); 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]; const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
expect(deviceSig).toHaveProperty("signatures"); expect(deviceSig).toHaveProperty("signatures");
}); });
+224 -246
View File
@@ -16,12 +16,12 @@ limitations under the License.
*/ */
import anotherjson from "another-json"; import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb"; import { IDBFactory } from "fake-indexeddb";
import Olm from "@matrix-org/olm"; 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 * as testUtils from "../../test-utils/test-utils";
import { import {
emitPromise, emitPromise,
@@ -86,9 +86,13 @@ import {
encryptGroupSessionKey, encryptGroupSessionKey,
encryptMegolmEvent, encryptMegolmEvent,
encryptMegolmEventRawPlainText, encryptMegolmEventRawPlainText,
encryptOlmEvent,
establishOlmSession, establishOlmSession,
getTestOlmAccountKeys, getTestOlmAccountKeys,
} from "./olm-utils"; expectSendRoomKey,
expectSendMegolmMessageEvent,
expectEncryptedSendMessageEvent,
} from "./olm-utils.ts";
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator"; import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event"; import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
import { KnownMembership } from "../../../src/@types/membership"; import { KnownMembership } from "../../../src/@types/membership";
@@ -101,110 +105,9 @@ afterEach(() => {
// eslint-disable-next-line no-global-assign // eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory(); indexedDB = new IDBFactory();
jest.useRealTimers(); vi.useRealTimers();
}); });
/**
* Expect that the client shares keys with the given recipient
*
* Waits for an HTTP request to send the encrypted m.room_key to-device message; decrypts it and uses it
* to establish an Olm InboundGroupSession.
*
* @param recipientUserID - the user id of the expected recipient
*
* @param recipientOlmAccount - Olm.Account for the recipient
*
* @param recipientOlmSession - an Olm.Session for the recipient, which must already have exchanged pre-key
* messages with the sender. Alternatively, null, in which case we will expect a pre-key message.
*
* @returns the established inbound group session
*/
async function expectSendRoomKey(
recipientUserID: string,
recipientOlmAccount: Olm.Account,
recipientOlmSession: Olm.Session | null = null,
): Promise<Olm.InboundGroupSession> {
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
function onSendRoomKey(content: any): Olm.InboundGroupSession {
const m = content.messages[recipientUserID].DEVICE_ID;
const ct = m.ciphertext[testRecipientKey];
if (!recipientOlmSession) {
expect(ct.type).toEqual(0); // pre-key message
recipientOlmSession = new Olm.Session();
recipientOlmSession.create_inbound(recipientOlmAccount, ct.body);
} else {
expect(ct.type).toEqual(1); // regular message
}
const decrypted = JSON.parse(recipientOlmSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual("m.room_key");
const inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
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,
},
);
});
}
/**
* Return the event received on rooms/{roomId}/send/m.room.encrypted endpoint.
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
* @returns the content of the encrypted event
*/
function expectEncryptedSendMessage() {
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 },
);
});
}
/**
* Expect that the client sends an encrypted event
*
* Waits for an HTTP request to send an encrypted message in the test room.
*
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
*
* @returns The content of the successfully-decrypted event
*/
async function expectSendMegolmMessage(
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
): Promise<Partial<IEvent>> {
const encryptedMessageContent = await expectEncryptedSendMessage();
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
const inboundGroupSession = await inboundGroupSessionPromise;
const r: any = inboundGroupSession.decrypt(encryptedMessageContent!.ciphertext);
logger.log("Decrypted received megolm message", r);
return JSON.parse(r.plaintext);
}
describe("crypto", () => { describe("crypto", () => {
let testOlmAccount = {} as unknown as Olm.Account; let testOlmAccount = {} as unknown as Olm.Account;
let testSenderKey = ""; let testSenderKey = "";
@@ -252,13 +155,8 @@ describe("crypto", () => {
return response; return response;
} }
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString()); const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
fetchMock.postOnce( fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/query"), (callLog) =>
new RegExp(rootRegexp + "(r0|v3)/keys/query"), onQueryRequest(JSON.parse(callLog.options.body as string)),
(url: string, opts: RequestInit) => onQueryRequest(JSON.parse(opts.body as string)),
{
// append to the list of intercepts on this path
overwriteRoutes: false,
},
); );
} }
@@ -268,7 +166,7 @@ describe("crypto", () => {
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult} * @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
* (or a function that returns one). * (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()); const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response); fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
} }
@@ -321,15 +219,20 @@ describe("crypto", () => {
*/ */
function createCryptoCallbacks(): CryptoCallbacks { function createCryptoCallbacks(): CryptoCallbacks {
// Store the cached secret storage key and return it when `getSecretStorageKey` is called // Store the cached secret storage key and return it when `getSecretStorageKey` is called
let cachedKey: { keyId: string; key: Uint8Array }; let cachedKey: { keyId: string; key: Uint8Array<ArrayBuffer> };
const cacheSecretStorageKey = (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => { const cacheSecretStorageKey = (
keyId: string,
keyInfo: SecretStorageKeyDescription,
key: Uint8Array<ArrayBuffer>,
) => {
cachedKey = { cachedKey = {
keyId, keyId,
key, key,
}; };
}; };
const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]); const getSecretStorageKey = () =>
Promise.resolve<[string, Uint8Array<ArrayBuffer>]>([cachedKey.keyId, cachedKey.key]);
return { return {
cacheSecretStorageKey, cacheSecretStorageKey,
@@ -341,7 +244,6 @@ describe("crypto", () => {
async () => { async () => {
// anything that we don't have a specific matcher for silently returns a 404 // anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404); fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com"; const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({ aliceClient = createClient({
@@ -363,18 +265,20 @@ describe("crypto", () => {
testOlmAccount = await createOlmAccount(); testOlmAccount = await createOlmAccount();
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys()); const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519; testSenderKey = testE2eKeys.curve25519;
vi.useRealTimers();
}, },
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */ /* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000, 10000,
); );
afterEach(async () => { afterEach(async () => {
await aliceClient.stopClient(); aliceClient.stopClient();
// Allow in-flight things to complete before we tear down the test // Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync(); if (vi.isFakeTimers()) {
await vi.runAllTimersAsync();
fetchMock.mockReset(); }
}); });
it("MatrixClient.getCrypto returns a CryptoApi", () => { it("MatrixClient.getCrypto returns a CryptoApi", () => {
@@ -441,7 +345,7 @@ describe("crypto", () => {
describe("Unable to decrypt error codes", function () { describe("Unable to decrypt error codes", function () {
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] }); vi.useFakeTimers();
}); });
it("Decryption fails with UISI error", async () => { it("Decryption fails with UISI error", async () => {
@@ -817,7 +721,7 @@ describe("crypto", () => {
syncResponder.sendOrQueueSyncResponse(syncResponse); syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient); await syncPromise(aliceClient);
await awaitDecryptionError; await expect(awaitDecryptionError).resolves.toBeUndefined();
}); });
}); });
@@ -968,6 +872,7 @@ describe("crypto", () => {
await expectSendRoomKey("@bob:xyz", testOlmAccount); await expectSendRoomKey("@bob:xyz", testOlmAccount);
}); });
// eslint-disable-next-line @vitest/expect-expect
it("Alice sends a megolm message", async () => { it("Alice sends a megolm message", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl(); const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl); const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -991,10 +896,11 @@ describe("crypto", () => {
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt. // Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
await Promise.all([ await Promise.all([
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
expectSendMegolmMessage(inboundGroupSessionPromise), expectSendMegolmMessageEvent(inboundGroupSessionPromise),
]); ]);
}); });
// eslint-disable-next-line @vitest/expect-expect
it("We should start a new megolm session after forceDiscardSession", async () => { it("We should start a new megolm session after forceDiscardSession", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl(); const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl); const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -1018,7 +924,7 @@ describe("crypto", () => {
// Send the first message, and check we can decrypt it. // Send the first message, and check we can decrypt it.
await Promise.all([ await Promise.all([
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
expectSendMegolmMessage(inboundGroupSessionPromise), expectSendMegolmMessageEvent(inboundGroupSessionPromise),
]); ]);
// Finally the interesting part: discard the session. // Finally the interesting part: discard the session.
@@ -1026,7 +932,7 @@ describe("crypto", () => {
// Now when we send the next message, we should get a *new* megolm session. // Now when we send the next message, we should get a *new* megolm session.
const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount); const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount);
const p2 = expectSendMegolmMessage(inboundGroupSessionPromise2); const p2 = expectSendMegolmMessageEvent(inboundGroupSessionPromise2);
await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]); await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]);
}); });
@@ -1037,7 +943,7 @@ describe("crypto", () => {
*/ */
async function sendEncryptedMessage(): Promise<IContent> { async function sendEncryptedMessage(): Promise<IContent> {
const [encryptedMessage] = await Promise.all([ const [encryptedMessage] = await Promise.all([
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
]); ]);
return encryptedMessage; return encryptedMessage;
@@ -1095,9 +1001,7 @@ describe("crypto", () => {
await startClientAndAwaitFirstSync(); await startClientAndAwaitFirstSync();
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount); 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 vi.useFakeTimers();
// working `queueMicrotask`
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
const syncResponse = getSyncResponse(["@bob:xyz"]); const syncResponse = getSyncResponse(["@bob:xyz"]);
@@ -1129,7 +1033,7 @@ describe("crypto", () => {
expect(sessionId).toBeDefined(); expect(sessionId).toBeDefined();
// Advance the time by 1h // Advance the time by 1h
jest.advanceTimersByTime(oneHourInMs); vi.advanceTimersByTime(oneHourInMs);
// Send a second message to bob and get the encrypted message // Send a second message to bob and get the encrypted message
const [secondEncryptedMessage] = await Promise.all([ const [secondEncryptedMessage] = await Promise.all([
@@ -1159,7 +1063,7 @@ describe("crypto", () => {
let [, , encryptedMessage] = await Promise.all([ let [, , encryptedMessage] = await Promise.all([
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
// Check that the session id exists // Check that the session id exists
@@ -1187,7 +1091,7 @@ describe("crypto", () => {
[, , encryptedMessage] = await Promise.all([ [, , encryptedMessage] = await Promise.all([
aliceClient.sendTextMessage(ROOM_ID, "test"), aliceClient.sendTextMessage(ROOM_ID, "test"),
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession), expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
// Check that the new session id exists // Check that the new session id exists
@@ -1250,7 +1154,7 @@ describe("crypto", () => {
// it probably won't be decrypted yet, because it takes a while to process the olm keys // 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 }); const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID); expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID);
expect(decryptedEvent.getContent()).toEqual({}); expect(decryptedEvent.getContent<IContent>()).toEqual({});
expect(decryptedEvent.getClearContent()).toBeUndefined(); expect(decryptedEvent.getClearContent()).toBeUndefined();
}); });
@@ -1274,20 +1178,12 @@ describe("crypto", () => {
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount); const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
// ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes. // ... 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) => { const reqProm = new Promise<IContent>((resolve) => {
fetchMock.putOnce( fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), async (callLog): Promise<RouteResponse> => {
new RegExp("/send/m.room.encrypted/"), resolve(JSON.parse(callLog.options.body as string));
async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => { return await sendRoomMessageResolvers.promise;
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,
},
);
}); });
// Now we start to send the message // Now we start to send the message
@@ -1370,6 +1266,7 @@ describe("crypto", () => {
}); });
} }
// eslint-disable-next-line @vitest/expect-expect
it("Sending an event initiates a member list sync", async () => { it("Sending an event initiates a member list sync", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl(); const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl); const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -1385,7 +1282,7 @@ describe("crypto", () => {
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession); const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
// and finally the megolm message // and finally the megolm message
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise); const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
// kick it off // kick it off
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test"); const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
@@ -1393,6 +1290,7 @@ describe("crypto", () => {
await Promise.all([sendPromise, megolmMessagePromise, memberListPromise]); await Promise.all([sendPromise, megolmMessagePromise, memberListPromise]);
}); });
// eslint-disable-next-line @vitest/expect-expect
it("loading the membership list inhibits a later load", async () => { it("loading the membership list inhibits a later load", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl(); const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl); const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -1408,7 +1306,7 @@ describe("crypto", () => {
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession); const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
// and finally the megolm message // and finally the megolm message
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise); const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
// kick it off // kick it off
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test"); const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
@@ -1491,8 +1389,10 @@ describe("crypto", () => {
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode); expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors. // `decryptionFailureReason` should be `MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE` for `m.unverified`
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified"); expect(
ev.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
).toEqual(withheldCode === "m.unverified");
}); });
}, },
); );
@@ -1500,33 +1400,26 @@ describe("crypto", () => {
describe("key upload request", () => { describe("key upload request", () => {
beforeEach(() => { beforeEach(() => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`. vi.useFakeTimers();
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
}); });
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> { function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
return new Promise((resolve) => { return new Promise((resolve) => {
const listener = (url: string, options: RequestInit) => { fetchMock.modifyRoute("keys-upload", {
const content = JSON.parse(options.body as string); response: (callLog) => {
const keysCount = Object.keys(content?.one_time_keys || {}).length; const content = JSON.parse(callLog.options.body as string);
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length; const keysCount = Object.keys(content?.one_time_keys || {}).length;
if (keysCount) resolve({ keysCount, fallbackKeysCount }); const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
return { if (keysCount) resolve({ keysCount, fallbackKeysCount });
one_time_key_counts: { return {
// The matrix client does `/upload` requests until 50 keys are uploaded one_time_key_counts: {
// We return here 60 to avoid the `/upload` request loop // The matrix client does `/upload` requests until 50 keys are uploaded
signed_curve25519: keysCount ? 60 : keysCount, // 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,
});
}
}); });
} }
@@ -1539,7 +1432,7 @@ describe("crypto", () => {
await syncPromise(aliceClient); 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; const { keysCount, fallbackKeysCount } = await uploadPromise;
expect(keysCount).toBeGreaterThan(0); expect(keysCount).toBeGreaterThan(0);
expect(fallbackKeysCount).toBe(0); expect(fallbackKeysCount).toBe(0);
@@ -1553,7 +1446,7 @@ describe("crypto", () => {
// Advance local date to 2 minutes // Advance local date to 2 minutes
// The old crypto only runs the upload every 60 seconds // 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); await syncPromise(aliceClient);
@@ -1644,18 +1537,16 @@ describe("crypto", () => {
function awaitKeyQueryRequest(): Promise<Record<string, []>> { function awaitKeyQueryRequest(): Promise<Record<string, []>> {
return new Promise((resolve) => { 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( fetchMock.post(
new URL("/_matrix/client/v3/keys/query", aliceClient.getHomeserverUrl()).toString(), 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;
},
); );
}); });
} }
@@ -1694,8 +1585,7 @@ describe("crypto", () => {
}); });
it("Get devices from tracked users", async () => { 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`. vi.useFakeTimers();
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} }); expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync(); await startClientAndAwaitFirstSync();
@@ -1707,12 +1597,12 @@ describe("crypto", () => {
// Advance local date to 2 minutes // Advance local date to 2 minutes
// The old crypto only runs the upload every 60 seconds // 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); await syncPromise(aliceClient);
// Old crypto: for alice: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList` // 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` // Old crypto: for alice: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
await flushPromises(); await flushPromises();
@@ -1720,7 +1610,7 @@ describe("crypto", () => {
await queryPromise; await queryPromise;
// Old crypto: for `user`: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList` // 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` // Old crypto: for `user`: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
// It will add `@testing_florian1:matrix.org` devices to the DeviceList // It will add `@testing_florian1:matrix.org` devices to the DeviceList
await flushPromises(); await flushPromises();
@@ -1744,7 +1634,7 @@ describe("crypto", () => {
* Create a fake secret storage key * Create a fake secret storage key
* Async because `bootstrapSecretStorage` expect an async method * 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 keyInfo: {}, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33), privateKey: Uint8Array.of(32, 33),
}); });
@@ -1762,7 +1652,7 @@ describe("crypto", () => {
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype * https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
*/ */
async function awaitCrossSigningKeyUpload(key: string): Promise<Record<string, {}>> { 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; return content.encrypted;
} }
@@ -1774,10 +1664,7 @@ describe("crypto", () => {
async function awaitSecretStorageKeyStoredInAccountData(): Promise<string> { async function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
const content = await accountDataAccumulator.interceptSetAccountData(":type(m.secret_storage.*)", { const content = await accountDataAccumulator.waitForAccountData("m.secret_storage.*");
repeat: 1,
overwriteRoutes: true,
});
if (content.key) { if (content.key) {
return content.key; return content.key;
} }
@@ -1785,10 +1672,7 @@ describe("crypto", () => {
} }
async function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> { async function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
const content = await accountDataAccumulator.interceptSetAccountData("m.megolm_backup.v1", { const content = await accountDataAccumulator.waitForAccountData("m.megolm_backup.v1");
repeat: 1,
overwriteRoutes: true,
});
return content.encrypted; return content.encrypted;
} }
@@ -1809,7 +1693,6 @@ describe("crypto", () => {
* @param backupVersion - The version of the created backup * @param backupVersion - The version of the created backup
*/ */
async function bootstrapSecurity(backupVersion: string): Promise<void> { async function bootstrapSecurity(backupVersion: string): Promise<void> {
mockSetupCrossSigningRequests();
mockSetupMegolmBackupRequests(backupVersion); mockSetupMegolmBackupRequests(backupVersion);
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true` // promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
@@ -1888,6 +1771,7 @@ describe("crypto", () => {
it("Should create a 4S key", async () => { it("Should create a 4S key", async () => {
accountDataAccumulator.interceptGetAccountData(); accountDataAccumulator.interceptGetAccountData();
accountDataAccumulator.interceptSetAccountData();
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key"); const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
@@ -2039,8 +1923,7 @@ describe("crypto", () => {
describe("Manage Key Backup", () => { describe("Manage Key Backup", () => {
beforeEach(async () => { beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`. vi.useFakeTimers();
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
}); });
it("Should be able to restore from 4S after bootstrap", async () => { it("Should be able to restore from 4S after bootstrap", async () => {
@@ -2057,29 +1940,23 @@ describe("crypto", () => {
const newKey = testData.MEGOLM_SESSION_DATA; const newKey = testData.MEGOLM_SESSION_DATA;
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => { const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
fetchMock.put( fetchMock.put("path:/_matrix/client/v3/room_keys/keys", (callLog) => {
"path:/_matrix/client/v3/room_keys/keys", const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
(url, request) => { resolve(uploadPayload);
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}"); return {
resolve(uploadPayload); status: 200,
return { body: {
status: 200, count: 1,
body: { etag: "abcdefg",
count: 1, },
etag: "abcdefg", };
}, });
};
},
{
overwriteRoutes: true,
},
);
}); });
await aliceClient.getCrypto()!.importRoomKeys([newKey]); await aliceClient.getCrypto()!.importRoomKeys([newKey]);
// The backup loop waits a random amount of time to avoid different clients firing at the same time. // 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; const keyBackupData = await awaitKeyUploaded;
@@ -2107,40 +1984,33 @@ describe("crypto", () => {
fetchMock.delete( fetchMock.delete(
"express:/_matrix/client/v3/room_keys/version/:version", "express:/_matrix/client/v3/room_keys/version/:version",
(url: string, options: RequestInit) => { (url: string, options: RequestInit) => {
fetchMock.get( fetchMock.modifyRoute("room-keys-version", {
"path:/_matrix/client/v3/room_keys/version", response: {
{
status: 404, status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." }, body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
}, },
{ overwriteRoutes: true }, });
);
resolve(); resolve();
return {}; return {};
}, },
{ overwriteRoutes: true },
); );
}); });
const newVersion = "2"; const newVersion = "2";
fetchMock.post( fetchMock.modifyRoute("post-room-keys-version", {
"path:/_matrix/client/v3/room_keys/version", response: (callLog) => {
(url, request) => { const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
backupData.version = newVersion; backupData.version = newVersion;
backupData.count = 0; backupData.count = 0;
backupData.etag = "zer"; backupData.etag = "zer";
// update get call with new version // update get call with new version
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, { fetchMock.modifyRoute("room-keys-version", { response: backupData });
overwriteRoutes: true,
});
return { return {
version: backupVersion, version: backupVersion,
}; };
}, },
{ overwriteRoutes: true }, });
);
const newBackupStatusUpdate = new Promise<void>((resolve) => { const newBackupStatusUpdate = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => { aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
@@ -2195,6 +2065,7 @@ describe("crypto", () => {
expect(hasCrossSigningKeysForUser).toBe(true); expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID); 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.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false); expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false); expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2202,15 +2073,15 @@ describe("crypto", () => {
}); });
it("Cross signing keys are available for a tracked user", async () => { it("Cross signing keys are available for a tracked user", async () => {
// Process Alice keys, old crypto has a sleep(5ms) during the process // Process Alice keys
await jest.advanceTimersByTimeAsync(5);
await flushPromises(); await flushPromises();
// Alice is the local user and should be tracked ! // Alice is the local user and should be tracked !
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID); const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true); 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.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false); expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false); expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2221,7 +2092,8 @@ describe("crypto", () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz"); const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
expect(hasCrossSigningKeysForUser).toBe(false); 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.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false); expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false); expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2251,6 +2123,7 @@ describe("crypto", () => {
{ {
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID); const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.known).toBe(true);
expect(verificationStatus.isVerified()).toBe(false); expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false); expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false); expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2291,6 +2164,7 @@ describe("crypto", () => {
client2?.stopClient(); 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 () => { 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 // Alice is in an encrypted room
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" }); const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
@@ -2298,7 +2172,7 @@ describe("crypto", () => {
await syncPromise(client1); await syncPromise(client1);
// Send a message, and expect to get an `m.room.encrypted` event. // Send a message, and expect to get an `m.room.encrypted` event.
await Promise.all([client1.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessage()]); await Promise.all([client1.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessageEvent()]);
// We now replace the client, and allow the new one to resync, *without* the encryption event. // We now replace the client, and allow the new one to resync, *without* the encryption event.
client2 = await replaceClient(client1); client2 = await replaceClient(client1);
@@ -2319,7 +2193,7 @@ describe("crypto", () => {
// Send a message, and expect to get an `m.room.encrypted` event. // Send a message, and expect to get an `m.room.encrypted` event.
const [, msg1Content] = await Promise.all([ const [, msg1Content] = await Promise.all([
client1.sendTextMessage(ROOM_ID, "test1"), client1.sendTextMessage(ROOM_ID, "test1"),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
// Replace the state with one which bumps the rotation period. This should be ignored, though it's not // Replace the state with one which bumps the rotation period. This should be ignored, though it's not
@@ -2338,16 +2212,17 @@ describe("crypto", () => {
// use a different one. // use a different one.
const [, msg2Content] = await Promise.all([ const [, msg2Content] = await Promise.all([
client1.sendTextMessage(ROOM_ID, "test2"), client1.sendTextMessage(ROOM_ID, "test2"),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
expect(msg2Content.session_id).toEqual(msg1Content.session_id); expect(msg2Content.session_id).toEqual(msg1Content.session_id);
const [, msg3Content] = await Promise.all([ const [, msg3Content] = await Promise.all([
client1.sendTextMessage(ROOM_ID, "test3"), client1.sendTextMessage(ROOM_ID, "test3"),
expectEncryptedSendMessage(), expectEncryptedSendMessageEvent(),
]); ]);
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id); 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 () => { 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 // 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 }); const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
@@ -2355,7 +2230,7 @@ describe("crypto", () => {
await syncPromise(client1); await syncPromise(client1);
// Send a message, and expect to get an `m.room.encrypted` event. // Send a message, and expect to get an `m.room.encrypted` event.
await Promise.all([client1.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessage()]); await Promise.all([client1.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessageEvent()]);
// We now replace the client, and allow the new one to resync with a *different* encryption event. // We now replace the client, and allow the new one to resync with a *different* encryption event.
client2 = await replaceClient(client1); client2 = await replaceClient(client1);
@@ -2441,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 "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 debug from "debug";
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src"; import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
@@ -28,7 +29,7 @@ import { emitPromise, EventCounter } from "../../test-utils/test-utils";
describe("Device dehydration", () => { describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => { it("should rehydrate and dehydrate a device", async () => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] }); vi.useFakeTimers();
const matrixClient = createClient({ const matrixClient = createClient({
baseUrl: "http://test.server", baseUrl: "http://test.server",
@@ -59,28 +60,35 @@ describe("Device dehydration", () => {
}); });
const crypto = matrixClient.getCrypto()!; const crypto = matrixClient.getCrypto()!;
fetchMock.config.overwriteRoutes = true;
// start dehydration -- we start with no dehydrated device, and we // start dehydration -- we start with no dehydrated device, and we
// store the dehydrated device that we create // store the dehydrated device that we create
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", { fetchMock.get(
status: 404, "path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
body: { {
errcode: "M_NOT_FOUND", status: 404,
error: "Not found", body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
}, },
}); { name: "get-dehydrated-device" },
);
let dehydratedDeviceBody: any; let dehydratedDeviceBody: any;
let dehydrationCount = 0; let dehydrationCount = 0;
let resolveDehydrationPromise: () => void; let resolveDehydrationPromise: () => void;
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => { fetchMock.put(
dehydratedDeviceBody = JSON.parse(opts.body as string); "path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
dehydrationCount++; (callLog) => {
if (resolveDehydrationPromise) { dehydratedDeviceBody = JSON.parse(callLog.options.body as string);
resolveDehydrationPromise(); dehydrationCount++;
} if (resolveDehydrationPromise) {
return {}; resolveDehydrationPromise();
}); }
return {};
},
{ name: "put-dehydrated-device" },
);
await crypto.startDehydration(); await crypto.startDehydration();
expect(dehydrationCount).toEqual(1); expect(dehydrationCount).toEqual(1);
@@ -91,7 +99,7 @@ describe("Device dehydration", () => {
const dehydrationPromise = new Promise<void>((resolve, reject) => { const dehydrationPromise = new Promise<void>((resolve, reject) => {
resolveDehydrationPromise = resolve; resolveDehydrationPromise = resolve;
}); });
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000); vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise; await dehydrationPromise;
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1); expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
@@ -101,16 +109,18 @@ describe("Device dehydration", () => {
// restart dehydration -- rehydrate the device that we created above, // restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so // and create a new dehydrated device. We also set `createNewKey`, so
// a new dehydration key will be set // a new dehydration key will be set
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", { fetchMock.modifyRoute("get-dehydrated-device", {
device_id: dehydratedDeviceBody.device_id, response: {
device_data: dehydratedDeviceBody.device_data, 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. // rehydrating should make two calls to the /events endpoint.
// The first time will return a single event, and the second // The first time will return a single event, and the second
// time will return no events (which will signal to the // time will return no events (which will signal to the
// rehydration function that it can stop) // 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 nextBatch = body.next_batch ?? "0";
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : []; const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
return { return {
@@ -135,28 +145,30 @@ describe("Device dehydration", () => {
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2); expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
// test that if we get an error when we try to rotate, it emits an event // 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", { fetchMock.modifyRoute("put-dehydrated-device", {
status: 500, response: {
body: { status: 500,
errcode: "M_UNKNOWN", body: {
error: "Unknown error", errcode: "M_UNKNOWN",
error: "Unknown error",
},
}, },
}); });
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError); const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000); vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await rotationErrorEventPromise; await rotationErrorEventPromise;
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails. // 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", { fetchMock.modifyRoute("get-dehydrated-device", {
status: 500, response: {
body: { status: 500,
errcode: "M_UNKNOWN", body: {
error: "Unknown error", errcode: "M_UNKNOWN",
error: "Unknown error",
},
}, },
}); });
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => { fetchMock.modifyRoute("put-dehydrated-device", { response: { body: {} } });
return {};
});
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError); const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
await crypto.startDehydration(true); await crypto.startDehydration(true);
await rehydrationErrorEventPromise; await rehydrationErrorEventPromise;
@@ -182,8 +194,8 @@ async function initializeSecretStorage(
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl); const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver); e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
const accountData: Map<string, object> = new Map(); const accountData: Map<string, object> = new Map();
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => { fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
const name = url.split("/").pop()!; const name = callLog.url.split("/").pop()!;
const value = accountData.get(name); const value = accountData.get(name);
if (value) { if (value) {
return value; return value;
@@ -197,9 +209,9 @@ async function initializeSecretStorage(
}; };
} }
}); });
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => { fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
const name = url.split("/").pop()!; const name = callLog.url.split("/").pop()!;
const value = JSON.parse(opts.body as string); const value = JSON.parse(callLog.options.body as string);
accountData.set(name, value); accountData.set(name, value);
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value })); matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
return {}; 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. limitations under the License.
*/ */
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb"; import { IDBFactory } from "fake-indexeddb";
import { type Mocked } from "jest-mock"; import { type Mocked } from "vitest";
import { import {
createClient, createClient,
encodeBase64, encodeBase64,
type IContent,
type ICreateClientOpts, type ICreateClientOpts,
type IEvent, type IEvent,
type IMegolmSessionData, type IMegolmSessionData,
@@ -36,7 +37,13 @@ import { advanceTimersUntil, awaitDecryption, syncPromise } from "../../test-uti
import * as testData from "../../test-utils/test-data"; import * as testData from "../../test-utils/test-data";
import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup"; import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { flushPromises } from "../../test-utils/flushPromises"; 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"; import { type KeyBackup } from "../../../src/rust-crypto/backup.ts";
const ROOM_ID = testData.TEST_ROOM_ID; const ROOM_ID = testData.TEST_ROOM_ID;
@@ -69,10 +76,11 @@ function mockUploadEmitter(
expectedVersion: string, expectedVersion: string,
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> { ): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
const emitter = new TypedEventEmitter(); const emitter = new TypedEventEmitter();
fetchMock.removeRoute("mock-upload-emitter");
fetchMock.put( fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys", "path:/_matrix/client/v3/room_keys/keys",
(url, request) => { (callLog) => {
const version = new URLSearchParams(new URL(url).search).get("version"); const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version != expectedVersion) { if (version != expectedVersion) {
return { return {
status: 403, 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; let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) { for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) { for (const sessionId of Object.keys(value.sessions)) {
@@ -99,9 +107,7 @@ function mockUploadEmitter(
}, },
}; };
}, },
{ { name: "mock-upload-emitter" },
overwriteRoutes: true,
},
); );
return emitter; return emitter;
} }
@@ -117,12 +123,10 @@ describe("megolm-keys backup", () => {
let e2eKeyResponder: E2EKeyResponder; let e2eKeyResponder: E2EKeyResponder;
beforeEach(async () => { beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`. vi.useFakeTimers();
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
// anything that we don't have a specific matcher for silently returns a 404 // anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404); fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
mockInitialApiRequests(TEST_HOMESERVER_URL); mockInitialApiRequests(TEST_HOMESERVER_URL);
syncResponder = new SyncResponder(TEST_HOMESERVER_URL); syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
@@ -133,15 +137,12 @@ describe("megolm-keys backup", () => {
}); });
afterEach(async () => { afterEach(async () => {
if (aliceClient !== undefined) { aliceClient?.stopClient();
await aliceClient.stopClient();
}
// Allow in-flight things to complete before we tear down the test // Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync(); await vi.runAllTimersAsync();
fetchMock.mockReset(); vi.restoreAllMocks();
jest.restoreAllMocks();
}); });
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> { 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 () => { 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 // 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") { if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA; return testData.CURVE25519_KEY_BACKUP_DATA;
} else { } else {
@@ -237,11 +238,11 @@ describe("megolm-keys backup", () => {
// Eventually, decryption succeeds. // Eventually, decryption succeeds.
await awaitDecryption(event, { waitOnDecryptionFailure: true }); 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 () => { it("handles error on backup query gracefully", async () => {
jest.spyOn(console, "error").mockImplementation(() => {}); vi.spyOn(console, "error").mockImplementation(() => {});
fetchMock.get( fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", "express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
@@ -253,9 +254,9 @@ describe("megolm-keys backup", () => {
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE); syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest(); await flushBackupRequest();
const calls = fetchMock.calls("getKey"); const calls = fetchMock.callHistory.calls("getKey");
expect(calls.length).toEqual(1); expect(calls.length).toEqual(1);
expect(calls[0][0]).toEqual(EXPECTED_URL); expect(calls[0].url).toEqual(EXPECTED_URL);
await flushBackupRequest(); await flushBackupRequest();
@@ -274,11 +275,11 @@ describe("megolm-keys backup", () => {
// Send Alice a message that she won't be able to decrypt // Send Alice a message that she won't be able to decrypt
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE); syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest(); await flushBackupRequest();
const calls = fetchMock.calls("getKey"); const calls = fetchMock.callHistory.calls("getKey");
expect(calls.length).toEqual(1); 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 // another message
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" }; const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
@@ -288,7 +289,7 @@ describe("megolm-keys backup", () => {
}; };
syncResponder.sendOrQueueSyncResponse(syncResponse2); syncResponder.sendOrQueueSyncResponse(syncResponse2);
await flushBackupRequest(); 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 () { 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 // @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 // We need several rooms with several sessions to test chunking
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]); const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
@@ -380,7 +381,7 @@ describe("megolm-keys backup", () => {
check!.backupInfo!.version!, check!.backupInfo!.version!,
); );
const progressCallback = jest.fn(); const progressCallback = vi.fn();
const result = await aliceCrypto.restoreKeyBackup({ const result = await aliceCrypto.restoreKeyBackup({
progressCallback, 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 () { it("Should continue to process backup if a chunk import fails and report failures", async function () {
const importMockImpl = jest const importMockImpl = vi
.fn() .fn()
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
// Fail to import first chunk // Fail to import first chunk
@@ -427,7 +428,7 @@ describe("megolm-keys backup", () => {
.mockResolvedValue(undefined); .mockResolvedValue(undefined);
// @ts-ignore - mock a private method for testing purpose // @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]); const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
@@ -439,7 +440,7 @@ describe("megolm-keys backup", () => {
check!.backupInfo!.version!, check!.backupInfo!.version!,
); );
const progressCallback = jest.fn(); const progressCallback = vi.fn();
const result = await aliceCrypto.restoreKeyBackup({ progressCallback }); const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
expect(result.total).toStrictEqual(expectedTotal); expect(result.total).toStrictEqual(expectedTotal);
@@ -463,13 +464,13 @@ describe("megolm-keys backup", () => {
it("Should continue if some keys fails to decrypt", async function () { it("Should continue if some keys fails to decrypt", async function () {
// @ts-ignore - mock a private method for testing purpose // @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = jest.fn(); aliceCrypto.importBackedUpRoomKeys = vi.fn();
const decryptionFailureCount = 2; const decryptionFailureCount = 2;
const mockDecryptor = { const mockDecryptor = {
// DecryptSessions does not reject on decryption failure, but just skip the key // 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 // simulate fail to decrypt 2 keys out of all
const decrypted = []; const decrypted = [];
const keys = Object.keys(sessions); const keys = Object.keys(sessions);
@@ -480,11 +481,11 @@ describe("megolm-keys backup", () => {
} }
return decrypted; return decrypted;
}), }),
free: jest.fn(), free: vi.fn(),
}; };
// @ts-ignore - mock a private method for testing purpose // @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]); 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 () { 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 // @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 = { const fullBackup = createFullBackup(
rooms: { testData.MEGOLM_SESSION_DATA.session_id,
[ROOM_ID]: { testData.CURVE25519_KEY_BACKUP_DATA,
sessions: { );
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup); fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage(); await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
@@ -526,15 +522,44 @@ describe("megolm-keys backup", () => {
expect(result.imported).toStrictEqual(1); 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 () => { 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"); 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", () => { describe("backupLoop", () => {
it("Alice should upload known keys when backup is enabled", async function () { it("Alice should upload known keys when backup is enabled", async function () {
// 404 means that there is no active backup // 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(); aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!; 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, { fetchMock.modifyRoute("room-keys-version", {
overwriteRoutes: true, response: { status: 200, body: testData.SIGNED_BACKUP_DATA },
}); });
const result = await aliceCrypto.checkKeyBackupAndEnable(); const result = await aliceCrypto.checkKeyBackupAndEnable();
@@ -581,7 +606,7 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys(someRoomKeys); await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time. // 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); await Promise.all(uploadPromises);
@@ -605,7 +630,7 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys([newKey]); await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers(); vi.runAllTimers();
await newKeyUploadPromise; await newKeyUploadPromise;
}); });
@@ -630,7 +655,7 @@ describe("megolm-keys backup", () => {
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY; const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
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, {
overwriteRoutes: true, name: "room-keys-version",
}); });
const result = await aliceCrypto.checkKeyBackupAndEnable(); const result = await aliceCrypto.checkKeyBackupAndEnable();
@@ -640,7 +665,7 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys(someRoomKeys); await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time. // 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 // wait for all keys to be backed up
await remainingZeroPromise; await remainingZeroPromise;
@@ -651,10 +676,7 @@ describe("megolm-keys backup", () => {
newBackup.version = newBackupVersion; newBackup.version = newBackupVersion;
// Let's simulate that a new backup is available by returning error code on key upload // Let's simulate that a new backup is available by returning error code on key upload
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
overwriteRoutes: true,
});
// If we import a new key the loop will try to upload to old version, it will // 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 // fail then check the current version and switch if trusted
@@ -697,12 +719,12 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys([newKey]); await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers(); vi.runAllTimers();
await disableOldBackup; await disableOldBackup;
await enableNewBackup; await enableNewBackup;
jest.runAllTimers(); vi.runAllTimers();
await Promise.all(uploadPromises); await Promise.all(uploadPromises);
await newKeyUploadPromise; await newKeyUploadPromise;
@@ -717,22 +739,14 @@ describe("megolm-keys backup", () => {
await waitForDeviceList(); await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); 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.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
overwriteRoutes: true,
});
// on the first key upload attempt, simulate a network failure // on the first key upload attempt, simulate a network failure
const failurePromise = new Promise((resolve) => { const failurePromise = new Promise((resolve) => {
fetchMock.put( fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
"path:/_matrix/client/v3/room_keys/keys", resolve(undefined);
() => { throw new TypeError(`Failed to fetch`);
resolve(undefined); });
throw new TypeError(`Failed to fetch`);
},
{
overwriteRoutes: true,
},
);
}); });
// kick the import loop off and wait for the failed request // kick the import loop off and wait for the failed request
@@ -741,27 +755,21 @@ describe("megolm-keys backup", () => {
const result = await aliceCrypto.checkKeyBackupAndEnable(); const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy(); expect(result).toBeTruthy();
jest.advanceTimersByTime(10 * 60 * 1000); vi.advanceTimersByTime(10 * 60 * 1000);
await failurePromise; await failurePromise;
// Fix the endpoint to do successful uploads // Fix the endpoint to do successful uploads
const successPromise = new Promise((resolve) => { const successPromise = new Promise((resolve) => {
fetchMock.put( fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
"path:/_matrix/client/v3/room_keys/keys", resolve(undefined);
() => { return {
resolve(undefined); status: 200,
return { body: {
status: 200, count: 2,
body: { etag: "abcdefg",
count: 2, },
etag: "abcdefg", };
}, });
};
},
{
overwriteRoutes: true,
},
);
}); });
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0` // 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 // 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 successPromise;
await allKeysUploadedPromise; await allKeysUploadedPromise;
}); });
@@ -782,7 +790,7 @@ describe("megolm-keys backup", () => {
it("getActiveSessionBackupVersion() should give correct result", async function () { it("getActiveSessionBackupVersion() should give correct result", async function () {
// 404 means that there is no active backup // 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(); aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!; const aliceCrypto = aliceClient.getCrypto()!;
@@ -801,9 +809,7 @@ describe("megolm-keys backup", () => {
// Serve a backup with no trusted signature // Serve a backup with no trusted signature
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA)); const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
delete unsignedBackup.auth_data.signatures; delete unsignedBackup.auth_data.signatures;
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, { fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", unsignedBackup);
overwriteRoutes: true,
});
const checked = await aliceCrypto.checkKeyBackupAndEnable(); const checked = await aliceCrypto.checkKeyBackupAndEnable();
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version); expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
@@ -813,9 +819,7 @@ describe("megolm-keys backup", () => {
expect(backupStatus).toBeNull(); expect(backupStatus).toBeNull();
// Add a valid signature to the backup // Add a valid signature to the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, { fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
overwriteRoutes: true,
});
// check that signalling is working // check that signalling is working
const backupPromise = new Promise<void>((resolve, reject) => { 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 () => { it("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
// 404 means that there is no active backup // 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}`, {}); fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
aliceClient = await initTestClient(); aliceClient = await initTestClient();
@@ -853,11 +857,9 @@ describe("megolm-keys backup", () => {
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull(); expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
// Return now the backup // Return now the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, { fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
overwriteRoutes: true,
});
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 // Delete the backup and we are expecting the key backup to be disabled
const keyBackupStatus = Promise.withResolvers<boolean>(); const keyBackupStatus = Promise.withResolvers<boolean>();
@@ -991,7 +993,7 @@ describe("megolm-keys backup", () => {
await waitForDeviceList(); await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); 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(); const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy(); expect(result).toBeTruthy();
@@ -1001,9 +1003,7 @@ describe("megolm-keys backup", () => {
delete unsignedBackup.auth_data.signatures; delete unsignedBackup.auth_data.signatures;
unsignedBackup.version = "2"; unsignedBackup.version = "2";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, { fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", unsignedBackup);
overwriteRoutes: true,
});
await aliceCrypto.checkKeyBackupAndEnable(); await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull(); expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
@@ -1018,7 +1018,7 @@ describe("megolm-keys backup", () => {
await waitForDeviceList(); await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); 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(); const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy(); expect(result).toBeTruthy();
@@ -1028,9 +1028,7 @@ describe("megolm-keys backup", () => {
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA)); const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
newBackup.version = newBackupVersion; newBackup.version = newBackupVersion;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, { fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", newBackup);
overwriteRoutes: true,
});
await aliceCrypto.checkKeyBackupAndEnable(); await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion); expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
@@ -1045,25 +1043,19 @@ describe("megolm-keys backup", () => {
await waitForDeviceList(); await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID); 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(); const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version); expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
fetchMock.get( fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", {
"path:/_matrix/client/v3/room_keys/version", status: 404,
{ body: {
status: 404, errcode: "M_NOT_FOUND",
body: { error: "No backup found",
errcode: "M_NOT_FOUND",
error: "No backup found",
},
}, },
{ });
overwriteRoutes: true,
},
);
const noResult = await aliceCrypto.checkKeyBackupAndEnable(); const noResult = await aliceCrypto.checkKeyBackupAndEnable();
expect(noResult).toBeNull(); expect(noResult).toBeNull();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull(); expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
@@ -1072,10 +1064,12 @@ describe("megolm-keys backup", () => {
describe("Backup Changed from other sessions", () => { describe("Backup Changed from other sessions", () => {
beforeEach(async () => { 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 // 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(); aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!; const aliceCrypto = aliceClient.getCrypto()!;
@@ -1108,9 +1102,9 @@ describe("megolm-keys backup", () => {
fetchMock.get( fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", "express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
(url, request) => { (callLog) => {
// check that the version is correct // 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") { if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA; return testData.CURVE25519_KEY_BACKUP_DATA;
} else { } 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. // 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]; const event = room.getLiveTimeline().getEvents()[0];
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true })); 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 // Second suppose now that the backup has changed to version 2
@@ -1146,7 +1140,7 @@ describe("megolm-keys backup", () => {
version: "2", 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 // suppose the new key is now known
const aliceCrypto = aliceClient.getCrypto()!; const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey( await aliceCrypto.storeSessionBackupPrivateKey(
@@ -1159,11 +1153,10 @@ describe("megolm-keys backup", () => {
const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>(); const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
fetchMock.get( fetchMock.modifyRoute("room-keys", {
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", response: (callLog) => {
(url, request) => {
// check that the version is correct // 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) { if (version == newBackup.version) {
awaitHasQueriedNewBackup.resolve(); awaitHasQueriedNewBackup.resolve();
return testData.CURVE25519_KEY_BACKUP_DATA; 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. // 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> = { const newMessage: Partial<IEvent> = {
@@ -1216,7 +1208,7 @@ describe("megolm-keys backup", () => {
// user will be one). // user will be one).
syncResponder.sendOrQueueSyncResponse({}); syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen // 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 // The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]); const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
+131
View File
@@ -16,6 +16,8 @@ limitations under the License.
import Olm from "@matrix-org/olm"; import Olm from "@matrix-org/olm";
import anotherjson from "another-json"; import anotherjson from "another-json";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
import { import {
type IContent, type IContent,
@@ -30,6 +32,7 @@ import { type IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { type ISyncResponder } from "../../test-utils/SyncResponder"; import { type ISyncResponder } from "../../test-utils/SyncResponder";
import { syncPromise } from "../../test-utils/test-utils"; import { syncPromise } from "../../test-utils/test-utils";
import { type KeyBackupInfo } from "../../../src/crypto-api"; import { type KeyBackupInfo } from "../../../src/crypto-api";
import { logger } from "../../../src/logger";
/** /**
* @module * @module
@@ -302,6 +305,9 @@ export function encryptMegolmEventRawPlainText(opts: {
}, },
type: "m.room.encrypted", type: "m.room.encrypted",
unsigned: {}, unsigned: {},
state_key: opts.plaintext.hasOwnProperty("state_key")
? `${opts.plaintext.type}:${opts.plaintext.state_key}`
: undefined,
}; };
} }
@@ -414,3 +420,128 @@ export async function establishOlmSession(
await syncPromise(testClient); await syncPromise(testClient);
return p2pSession; return p2pSession;
} }
/**
* Expect that the client shares keys with the given recipient
*
* Waits for an HTTP request to send the encrypted m.room_key to-device message; decrypts it and uses it
* to establish an Olm InboundGroupSession.
*
* @param recipientUserID - the user id of the expected recipient
*
* @param recipientOlmAccount - Olm.Account for the recipient
*
* @param recipientOlmSession - an Olm.Session for the recipient, which must already have exchanged pre-key
* messages with the sender. Alternatively, null, in which case we will expect a pre-key message.
*
* @returns the established inbound group session
*/
export async function expectSendRoomKey(
recipientUserID: string,
recipientOlmAccount: Olm.Account,
recipientOlmSession: Olm.Session | null = null,
): Promise<Olm.InboundGroupSession> {
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
function onSendRoomKey(content: any): Olm.InboundGroupSession {
const m = content.messages[recipientUserID].DEVICE_ID;
const ct = m.ciphertext[testRecipientKey];
if (!recipientOlmSession) {
expect(ct.type).toEqual(0); // pre-key message
recipientOlmSession = new Olm.Session();
recipientOlmSession.create_inbound(recipientOlmAccount, ct.body);
} else {
expect(ct.type).toEqual(1); // regular message
}
const decrypted = JSON.parse(recipientOlmSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual("m.room_key");
const inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return inboundGroupSession;
}
return await new Promise<Olm.InboundGroupSession>((resolve) => {
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
const content = JSON.parse(callLog.options.body as string);
resolve(onSendRoomKey(content));
return {};
});
});
}
/**
* Return the event received on rooms/{roomId}/send/m.room.encrypted endpoint.
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
* @returns the content of the encrypted event
*/
export function expectEncryptedSendMessageEvent() {
return new Promise<IContent>((resolve) => {
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" };
});
});
}
/**
* Return the event received on rooms/{roomId}/state/m.room.encrypted/{stateKey} endpoint.
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
* @returns the content of the encrypted event
*/
function expectEncryptedSendStateEvent() {
return new Promise<IContent>((resolve) => {
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" };
});
});
}
/**
* Expect that the client sends an encrypted message event
*
* Waits for an HTTP request to send an encrypted message in the test room.
*
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
*
* @returns The content of the successfully-decrypted event
*/
export async function expectSendMegolmMessageEvent(
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
): Promise<Partial<IEvent>> {
const encryptedMessageContent = await expectEncryptedSendMessageEvent();
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
const inboundGroupSession = await inboundGroupSessionPromise;
const r: any = inboundGroupSession.decrypt(encryptedMessageContent!.ciphertext);
logger.log("Decrypted received megolm message", r);
return JSON.parse(r.plaintext);
}
/**
* Expect that the client sends an encrypted state event
*
* Waits for an HTTP request to send an encrypted state event in the test room.
*
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
*
* @returns The content of the successfully-decrypted state event
*/
export async function expectSendMegolmStateEvent(
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
): Promise<Partial<IEvent>> {
const encryptedStateContent = await expectEncryptedSendStateEvent();
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
const inboundGroupSession = await inboundGroupSessionPromise;
const r: any = inboundGroupSession.decrypt(encryptedStateContent!.ciphertext);
logger.log("Decrypted received megolm state event", r);
return JSON.parse(r.plaintext);
}
+6 -8
View File
@@ -16,7 +16,7 @@ limitations under the License.
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb"; import { IDBFactory } from "fake-indexeddb";
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import { createClient, IndexedDBCryptoStore } from "../../../src"; import { createClient, IndexedDBCryptoStore } from "../../../src";
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump"; 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 { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
import { CryptoEvent } from "../../../src/crypto-api"; import { CryptoEvent } from "../../../src/crypto-api";
jest.setTimeout(15000); vi.setConfig({ testTimeout: 15000 });
afterEach(() => { afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections // 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 () => { it("should ignore a second call", async () => {
const matrixClient = createClient({ const matrixClient = createClient({
baseUrl: "http://test.server", baseUrl: "http://test.server",
@@ -134,10 +135,6 @@ describe("MatrixClient.initRustCrypto", () => {
}); });
describe("Libolm Migration", () => { describe("Libolm Migration", () => {
beforeEach(() => {
fetchMock.reset();
});
it("should migrate from libolm", async () => { it("should migrate from libolm", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse); 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, pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
}); });
const progressListener = jest.fn(); const progressListener = vi.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener); matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto(); await matrixClient.initRustCrypto();
@@ -326,7 +323,7 @@ describe("MatrixClient.initRustCrypto", () => {
}); });
// When we start Rust crypto, potentially triggering an upgrade // When we start Rust crypto, potentially triggering an upgrade
const progressListener = jest.fn(); const progressListener = vi.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener); matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto(); await matrixClient.initRustCrypto();
@@ -478,6 +475,7 @@ describe("MatrixClient.clearStores", () => {
expect(await indexedDB.databases()).toHaveLength(0); expect(await indexedDB.databases()).toHaveLength(0);
}); });
// eslint-disable-next-line @vitest/expect-expect
it("should not fail in environments without indexedDB", async () => { it("should not fail in environments without indexedDB", async () => {
// eslint-disable-next-line no-global-assign // eslint-disable-next-line no-global-assign
indexedDB = undefined!; indexedDB = undefined!;
+221
View File
@@ -0,0 +1,221 @@
/*
Copyright 2025 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 anotherjson from "another-json";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import Olm from "@matrix-org/olm";
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,
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";
import {
createOlmAccount,
createOlmSession,
encryptGroupSessionKey,
encryptMegolmEvent,
getTestOlmAccountKeys,
expectSendRoomKey,
expectSendMegolmStateEvent,
} from "./olm-utils";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
describe("Encrypted State Events", () => {
let testOlmAccount = {} as unknown as Olm.Account;
let testSenderKey = "";
/** the MatrixClient under test */
let aliceClient: MatrixClient;
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
let keyReceiver: E2EKeyReceiver;
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: ISyncResponder;
async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise<void> {
logger.log(aliceClient.getUserId() + ": starting");
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
syncResponder.sendOrQueueSyncResponse({ next_batch: 1 });
aliceClient.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: PendingEventOrdering.Detached,
...opts,
});
await syncPromise(aliceClient);
logger.log(aliceClient.getUserId() + ": started");
}
beforeEach(async () => {
fetchMock.catch(404);
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: "@alice:localhost",
accessToken: "akjgkrgjs",
deviceId: "xzcvb",
logger: logger.getChild("aliceClient"),
enableEncryptedStateEvents: true,
});
keyReceiver = new E2EKeyReceiver(homeserverUrl);
syncResponder = new SyncResponder(homeserverUrl);
await aliceClient.initRustCrypto();
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
testOlmAccount = await createOlmAccount();
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
}, 10000);
afterEach(async () => {
aliceClient.stopClient();
});
function expectAliceKeyQuery(response: any) {
fetchMock.postOnce(new RegExp("/keys/query"), (callLog) => response);
}
function expectAliceKeyClaim(response: any) {
fetchMock.postOnce(new RegExp("/keys/claim"), response);
}
function getTestKeysClaimResponse(userId: string) {
testOlmAccount.generate_one_time_keys(1);
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
testOlmAccount.mark_keys_as_published();
const keyId = Object.keys(testOneTimeKeys.curve25519)[0];
const oneTimeKey: string = testOneTimeKeys.curve25519[keyId];
const unsignedKeyResult = { key: oneTimeKey };
const j = anotherjson.stringify(unsignedKeyResult);
const sig = testOlmAccount.sign(j);
const keyResult = {
...unsignedKeyResult,
signatures: { [userId]: { "ed25519:DEVICE_ID": sig } },
};
return {
one_time_keys: { [userId]: { DEVICE_ID: { ["signed_curve25519:" + keyId]: keyResult } } },
failures: {},
};
}
it("Should receive an encrypted state event", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a state event with the group session
const eventEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
plaintext: {
type: "m.room.topic",
state_key: "",
content: {
topic: "Secret!",
},
},
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [eventEncrypted] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
// 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.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);
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
keyResponder.addDeviceKeys(testDeviceKeys);
await startClientAndAwaitFirstSync();
// Alice shares a room with Bob
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], HistoryVisibility.Joined, ROOM_ID, true));
await syncPromise(aliceClient);
// ... and claim one of Bob's OTKs ...
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
// ... and send an m.room.topic message
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
await Promise.all([
aliceClient.setRoomTopic(ROOM_ID, "Secret!"),
expectSendMegolmStateEvent(inboundGroupSessionPromise),
]);
});
});
+1 -3
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb"; import { IDBFactory } from "fake-indexeddb";
import Olm from "@matrix-org/olm"; import Olm from "@matrix-org/olm";
@@ -59,7 +59,6 @@ describe("to-device-messages", () => {
async () => { async () => {
// anything that we don't have a specific matcher for silently returns a 404 // anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404); fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://server.com"; const homeserverUrl = "https://server.com";
aliceClient = createClient({ aliceClient = createClient({
@@ -100,7 +99,6 @@ describe("to-device-messages", () => {
afterEach(async () => { afterEach(async () => {
aliceClient.stopClient(); aliceClient.stopClient();
fetchMock.mockReset();
}); });
describe("encryptToDeviceMessages", () => { describe("encryptToDeviceMessages", () => {
+141 -91
View File
@@ -18,12 +18,12 @@ import "fake-indexeddb/auto";
import anotherjson from "another-json"; import anotherjson from "another-json";
import debug from "debug"; 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 { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto"; import { createHash } from "crypto";
import Olm from "@matrix-org/olm"; import Olm from "@matrix-org/olm";
import type FetchMock from "fetch-mock";
import { import {
createClient, createClient,
DebugLogger, DebugLogger,
@@ -46,8 +46,8 @@ import {
type Verifier, type Verifier,
VerifierEvent, VerifierEvent,
} from "../../../src/crypto-api/verification"; } from "../../../src/crypto-api/verification";
import { escapeRegExp } from "../../../src/utils"; import { escapeRegExp, sleep } from "../../../src/utils";
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise } from "../../test-utils/test-utils"; import { awaitDecryption, emitPromise, getSyncResponse, syncPromise, waitFor } from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder"; import { SyncResponder } from "../../test-utils/SyncResponder";
import { import {
BACKUP_DECRYPTION_KEY_BASE64, BACKUP_DECRYPTION_KEY_BASE64,
@@ -79,11 +79,6 @@ import {
import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api"; import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api";
import { encodeBase64 } from "../../../src/base64"; import { encodeBase64 } from "../../../src/base64";
// 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"] });
beforeAll(async () => { beforeAll(async () => {
// we use the libolm primitives in the test, so init the Olm library // we use the libolm primitives in the test, so init the Olm library
await Olm.init(); await Olm.init();
@@ -96,6 +91,10 @@ beforeAll(async () => {
await RustSdkCryptoJs.initAsync(); await RustSdkCryptoJs.initAsync();
}, 10000); }, 10000);
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => { afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections // reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state // cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
@@ -128,7 +127,6 @@ describe("verification", () => {
beforeEach(async () => { beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404 // anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404); fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL); e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL); e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
@@ -139,14 +137,12 @@ describe("verification", () => {
}); });
afterEach(async () => { afterEach(async () => {
if (aliceClient !== undefined) { aliceClient?.stopClient();
await aliceClient.stopClient();
}
// Allow in-flight things to complete before we tear down the test // Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync(); if (vi.isFakeTimers()) {
await vi.runAllTimersAsync();
fetchMock.mockReset(); }
}); });
describe("Outgoing verification requests for another device", () => { describe("Outgoing verification requests for another device", () => {
@@ -154,11 +150,10 @@ describe("verification", () => {
// pretend that we have another device, which we will verify // pretend that we have another device, which we will verify
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA); e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
fetchMock.put( fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`), {
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`), ok: false,
{ ok: false, status: 404 }, status: 404,
{ overwriteRoutes: true }, });
);
}); });
// test with (1) the default verification method list, (2) a custom verification method list. // test with (1) the default verification method list, (2) a custom verification method list.
@@ -210,7 +205,7 @@ describe("verification", () => {
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId); expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(transactionId); expect(toDeviceMessage.transaction_id).toEqual(transactionId);
if (methods !== undefined) { 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)); expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
} }
@@ -243,7 +238,7 @@ describe("verification", () => {
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept"); const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify(); const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads // advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10); vi.advanceTimersByTime(10);
requestBody = await sendToDevicePromise; requestBody = await sendToDevicePromise;
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
@@ -321,7 +316,7 @@ describe("verification", () => {
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true); expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads // 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 // And now Alice starts a SAS verification
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start"); let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
@@ -514,7 +509,7 @@ describe("verification", () => {
// Rust crypto waits for the 'done' to arrive from the other side. // Rust crypto waits for the 'done' to arrive from the other side.
if (request.phase === VerificationPhase.Done) { if (request.phase === VerificationPhase.Done) {
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID); 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(); expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
await verificationPromise; await verificationPromise;
} }
@@ -637,7 +632,7 @@ describe("verification", () => {
expect(request.verifier).toBeUndefined(); expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads // 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 // ... but Alice wants to do an SAS verification
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start"); const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
@@ -682,7 +677,7 @@ describe("verification", () => {
expect(request.verifier).toBeUndefined(); expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads // 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 // ... but the dummy device wants to do an SAS verification
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId)); returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
@@ -735,6 +730,35 @@ describe("verification", () => {
expect(request.cancellingUserId).toEqual("@alice:localhost"); expect(request.cancellingUserId).toEqual("@alice:localhost");
}); });
it("does not include cancelled requests in the list of requests", async () => {
// Given Alice started a verification request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId!;
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
await waitForVerificationRequestChanged(request);
// Sanity: the request is listed
const requestsBeforeCancel = aliceClient
.getCrypto()!
.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
expect(requestsBeforeCancel).toHaveLength(1);
// When Alice cancels it
await Promise.all([expectSendToDeviceMessage("m.key.verification.cancel"), request.cancel()]);
// Then it is no longer listed as in progress
const requestsAfterCancel = aliceClient
.getCrypto()!
.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
expect(requestsAfterCancel).toHaveLength(0);
});
it("can cancel during the SAS phase", async () => { it("can cancel during the SAS phase", async () => {
// have alice initiate a verification. She should send a m.key.verification.request // have alice initiate a verification. She should send a m.key.verification.request
const [, request] = await Promise.all([ const [, request] = await Promise.all([
@@ -761,7 +785,7 @@ describe("verification", () => {
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept"); const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify(); const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads // advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10); vi.advanceTimersByTime(10);
await sendToDevicePromise; await sendToDevicePromise;
// now we unceremoniously cancel. We expect the verificatationPromise to reject. // now we unceremoniously cancel. We expect the verificatationPromise to reject.
@@ -906,19 +930,16 @@ describe("verification", () => {
function awaitRoomMessageRequest(): Promise<IContent> { function awaitRoomMessageRequest(): Promise<IContent> {
return new Promise((resolve) => { return new Promise((resolve) => {
// Case of unencrypted message of the new crypto // Case of unencrypted message of the new crypto
fetchMock.put( fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", (callLog) => {
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", resolve(JSON.parse(callLog.options.body as string));
(url: string, options: RequestInit) => { return { event_id: "$YUwRidLecu:example.com" };
resolve(JSON.parse(options.body as string)); });
return { event_id: "$YUwRidLecu:example.com" };
},
);
// Case of encrypted message of the old crypto // Case of encrypted message of the old crypto
fetchMock.put( fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId", "express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
async (url: string, options: RequestInit) => { async (callLog) => {
const encryptedMessage = JSON.parse(options.body as string); const encryptedMessage = JSON.parse(callLog.options.body as string);
const event = new MatrixEvent({ const event = new MatrixEvent({
content: encryptedMessage, content: encryptedMessage,
type: "m.room.encrypted", type: "m.room.encrypted",
@@ -941,7 +962,7 @@ describe("verification", () => {
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms // In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
// 5ms by users, ie Bob and Alice // 5ms by users, ie Bob and Alice
await jest.advanceTimersByTimeAsync(10); await vi.advanceTimersByTimeAsync(10);
const messageRequestPromise = awaitRoomMessageRequest(); const messageRequestPromise = awaitRoomMessageRequest();
const verificationRequest = await aliceClient const verificationRequest = await aliceClient
@@ -1051,7 +1072,14 @@ describe("verification", () => {
}); });
it("ignores old verification requests", async () => { it("ignores old verification requests", async () => {
const eventHandler = 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 = vi.fn();
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler); aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
const verificationRequestEvent = createVerificationRequestEvent(); const verificationRequestEvent = createVerificationRequestEvent();
@@ -1065,6 +1093,16 @@ describe("verification", () => {
const matrixEvent = room.getLiveTimeline().getEvents()[0]; const matrixEvent = room.getLiveTimeline().getEvents()[0];
expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id); expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id);
// Wait until the request has been processed. We use a real sleep()
// here to make sure any background async tasks are completed.
vi.useRealTimers();
await waitFor(async () => {
expect(info).toHaveBeenCalledWith(
expect.stringMatching(/^Ignoring just-received verification request/),
);
sleep(100);
});
// check that an event has not been raised, and that the request is not found // check that an event has not been raised, and that the request is not found
expect(eventHandler).not.toHaveBeenCalled(); expect(eventHandler).not.toHaveBeenCalled();
expect( expect(
@@ -1072,6 +1110,29 @@ describe("verification", () => {
).not.toBeDefined(); ).not.toBeDefined();
}); });
it("ignores cancelled verification requests", async () => {
// Given a verification request exists
const event = createVerificationRequestEvent();
returnRoomMessageFromSync(TEST_ROOM_ID, event);
// Wait for the request to be received
await emitPromise(aliceClient, CryptoEvent.VerificationRequestReceived);
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// When I cancel it
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.key.verification.cancel/:id", {
event_id: event.event_id,
});
await request!.cancel();
expect(request!.phase).toEqual(VerificationPhase.Cancelled);
// Then it is no longer found
expect(
aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz"),
).not.toBeDefined();
});
it("Plaintext verification request from Bob to Alice", async () => { it("Plaintext verification request from Bob to Alice", async () => {
// Add verification request from Bob to Alice in the DM between them // Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createVerificationRequestEvent()); returnRoomMessageFromSync(TEST_ROOM_ID, createVerificationRequestEvent());
@@ -1116,7 +1177,7 @@ describe("verification", () => {
returnToDeviceMessageFromSync(toDeviceEvent); returnToDeviceMessageFromSync(toDeviceEvent);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads // 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 // Wait for the request to be decrypted
const request1 = await requestEventPromise; const request1 = await requestEventPromise;
@@ -1153,7 +1214,7 @@ describe("verification", () => {
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted"); expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// Advance time by 5mins, the verification request should be ignored after that // 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 // Send Bob the room keys
returnToDeviceMessageFromSync(toDeviceEvent); returnToDeviceMessageFromSync(toDeviceEvent);
@@ -1219,7 +1280,7 @@ describe("verification", () => {
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID])); syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
await syncPromise(aliceClient); await syncPromise(aliceClient);
// DeviceList has a sleep(5) which we need to make happen // 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 // The client should now know about the olm device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]); const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
@@ -1231,11 +1292,10 @@ describe("verification", () => {
testOlmAccount?.free(); testOlmAccount?.free();
// Allow in-flight things to complete before we tear down the test // Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync(); await vi.runAllTimersAsync();
fetchMock.mockReset();
}); });
// eslint-disable-next-line @vitest/expect-expect
it("Should request cross signing keys after verification", async () => { it("Should request cross signing keys after verification", async () => {
const requestPromises = mockSecretRequestAndGetPromises(); const requestPromises = mockSecretRequestAndGetPromises();
@@ -1353,11 +1413,11 @@ describe("verification", () => {
*/ */
async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> { async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> {
// We are lacking a way to signal that the secret has been received, so we wait a bit.. // 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) => { await new Promise((resolve) => {
setTimeout(resolve, 500); setTimeout(resolve, 500);
}); });
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] }); vi.useFakeTimers();
return aliceClient.getCrypto()!.getSessionBackupPrivateKey(); return aliceClient.getCrypto()!.getSessionBackupPrivateKey();
} }
@@ -1390,27 +1450,21 @@ describe("verification", () => {
}); });
const expectBackupCheck = new Promise((resolve) => { const expectBackupCheck = new Promise((resolve) => {
fetchMock.get( fetchMock.get("express:/_matrix/client/v3/room_keys/version", (callLog) => {
"express:/_matrix/client/v3/room_keys/version", resolve(undefined);
(url, request) => { if (expectBackup instanceof MatrixError) {
resolve(undefined); return {
if (expectBackup instanceof MatrixError) { status: expectBackup.httpStatus,
return { body: expectBackup.data,
status: expectBackup.httpStatus, };
body: expectBackup.data, }
};
}
if (expectBackup instanceof Error) { if (expectBackup instanceof Error) {
return Promise.reject(expectBackup); return Promise.reject(expectBackup);
} }
return expectBackup; return expectBackup;
}, });
{
overwriteRoutes: true,
},
);
}); });
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA); fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
@@ -1491,7 +1545,7 @@ describe("verification", () => {
// user will be one). // user will be one).
syncResponder.sendOrQueueSyncResponse({}); syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen // 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 // The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]); const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
@@ -1525,8 +1579,8 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
return new Promise((resolve) => { return new Promise((resolve) => {
fetchMock.putOnce( fetchMock.putOnce(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`), new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
(url: string, opts: RequestInit): FetchMock.MockResponse => { (callLog): RouteResponse => {
resolve(JSON.parse(opts.body as string)); resolve(JSON.parse(callLog.options.body as string));
return {}; return {};
}, },
); );
@@ -1547,29 +1601,25 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
const uskRequestResolvers = Promise.withResolvers<string>(); const uskRequestResolvers = Promise.withResolvers<string>();
const backupKeyRequestResolvers = Promise.withResolvers<string>(); const backupKeyRequestResolvers = Promise.withResolvers<string>();
fetchMock.put( fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`), (callLog): RouteResponse => {
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`), const messages = JSON.parse(callLog.options.body as string).messages[TEST_USER_ID];
(url: string, opts: RequestInit): FetchMock.MockResponse => { // rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID]; const content = Object.values(messages)[0] as any;
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one if (content.action == "request") {
const content = Object.values(messages)[0] as any; const name = content.name;
if (content.action == "request") { const requestId = content.request_id;
const name = content.name; if (name == "m.cross_signing.user_signing") {
const requestId = content.request_id; uskRequestResolvers.resolve(requestId);
if (name == "m.cross_signing.user_signing") { } else if (name == "m.cross_signing.master") {
uskRequestResolvers.resolve(requestId); mskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.master") { } else if (name == "m.cross_signing.self_signing") {
mskRequestResolvers.resolve(requestId); sskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.self_signing") { } else if (name == "m.megolm_backup.v1") {
sskRequestResolvers.resolve(requestId); backupKeyRequestResolvers.resolve(requestId);
} else if (name == "m.megolm_backup.v1") {
backupKeyRequestResolvers.resolve(requestId);
}
} }
return {}; }
}, return {};
{ overwriteRoutes: true }, });
);
const promiseMap = new Map<string, Promise<string>>(); const promiseMap = new Map<string, Promise<string>>();
promiseMap.set("m.cross_signing.master", mskRequestResolvers.promise); 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(); 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 // @ts-ignore
client.clientOpts.threadSupport = true; client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental); Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
}); });
return Promise.all([ 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(), 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 // @ts-ignore
client.clientOpts.threadSupport = true; client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental); Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
}); });
return Promise.all([ 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(), httpBackend.flushAllExpected(),
]); ]);
}); });
@@ -2044,6 +2044,7 @@ describe("MatrixClient event timelines", function () {
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY); expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
} }
// eslint-disable-next-line @vitest/expect-expect
it("in stable mode", async () => { it("in stable mode", async () => {
// @ts-ignore // @ts-ignore
client.clientOpts.threadSupport = true; client.clientOpts.threadSupport = true;
+10 -13
View File
@@ -347,6 +347,7 @@ describe("MatrixClient", function () {
expect((await prom).room_id).toBe(roomId); 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 () { it("should no-op if you've already knocked a room", function () {
const room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
@@ -380,23 +381,16 @@ describe("MatrixClient", function () {
[ [
403, 403,
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" }, { errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
"[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]", "MatrixError: [403] You don't have permission to knock",
],
[
500,
{ errcode: "INTERNAL_SERVER_ERROR" },
"[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]",
], ],
[500, { errcode: "INTERNAL_SERVER_ERROR" }, "MatrixError: [500] Unknown message"],
]; ];
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => { it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error }); httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
const prom = client.knockRoom(roomId); const prom = client.knockRoom(roomId);
await Promise.all([ await Promise.all([httpBackend.flushAllExpected(), expect(prom).rejects.toThrow(snapshot)]);
httpBackend.flushAllExpected(),
expect(prom).rejects.toMatchInlineSnapshot(snapshot),
]);
}); });
}); });
}); });
@@ -1198,7 +1192,7 @@ describe("MatrixClient", function () {
describe("logout", () => { describe("logout", () => {
it("should abort pending requests when called with stopClient=true", async () => { it("should abort pending requests when called with stopClient=true", async () => {
httpBackend.when("POST", "/logout").respond(200, {}); httpBackend.when("POST", "/logout").respond(200, {});
const fn = jest.fn(); const fn = vi.fn();
client.http.request(Method.Get, "/test").catch(fn); client.http.request(Method.Get, "/test").catch(fn);
client.logout(true); client.logout(true);
await httpBackend.flush(undefined); await httpBackend.flush(undefined);
@@ -1326,7 +1320,7 @@ describe("MatrixClient", function () {
}); });
afterEach(() => { afterEach(() => {
jest.useRealTimers(); vi.useRealTimers();
}); });
it("should always fetch capabilities and then cache", async () => { it("should always fetch capabilities and then cache", async () => {
@@ -1397,6 +1391,7 @@ describe("MatrixClient", function () {
}); });
describe("publicRooms", () => { describe("publicRooms", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should use GET request if no server or filter is specified", () => { it("should use GET request if no server or filter is specified", () => {
httpBackend.when("GET", "/publicRooms").respond(200, {}); httpBackend.when("GET", "/publicRooms").respond(200, {});
client.publicRooms({}); client.publicRooms({});
@@ -1585,7 +1580,7 @@ describe("MatrixClient", function () {
describe("setSyncPresence", () => { describe("setSyncPresence", () => {
it("should pass calls through to the underlying sync api", () => { it("should pass calls through to the underlying sync api", () => {
const setPresence = jest.fn(); const setPresence = vi.fn();
// @ts-ignore // @ts-ignore
client.syncApi = { setPresence }; client.syncApi = { setPresence };
client.setSyncPresence(SetPresence.Unavailable); client.setSyncPresence(SetPresence.Unavailable);
@@ -1594,6 +1589,7 @@ describe("MatrixClient", function () {
}); });
describe("sendTyping", () => { describe("sendTyping", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should bail early for guests", async () => { it("should bail early for guests", async () => {
client.setGuest(true); client.setGuest(true);
await client.sendTyping("!room:server", true, 100); await client.sendTyping("!room:server", true, 100);
@@ -1851,6 +1847,7 @@ describe("MatrixClient", function () {
}); });
describe("setRoomMutePushRule", () => { describe("setRoomMutePushRule", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should set room push rule to muted", async () => { it("should set room push rule to muted", async () => {
const roomId = "!roomId:server"; const roomId = "!roomId:server";
const client = new MatrixClient({ const client = new MatrixClient({
+1 -1
View File
@@ -159,7 +159,7 @@ describe("MatrixClient opts", function () {
await expect( await expect(
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]), 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 () => { it("shouldn't queue events", async () => {
@@ -720,7 +720,7 @@ describe("MatrixClient room timelines", function () {
} else { } else {
reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire.")); 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 () => { room.on(RoomEvent.TimelineReset, async () => {
try { try {
@@ -15,7 +15,7 @@ limitations under the License.
*/ */
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src"; import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
@@ -83,8 +83,7 @@ describe("MatrixClient syncing errors", () => {
}); });
it("should retry, until errors are solved.", async () => { it("should retry, until errors are solved.", async () => {
jest.useFakeTimers(); vi.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
fetchMock fetchMock
.getOnce("end:versions", {}) // first version check without credentials needs to succeed .getOnce("end:versions", {}) // first version check without credentials needs to succeed
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry .getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
@@ -105,19 +104,18 @@ describe("MatrixClient syncing errors", () => {
await client!.startClient(); await client!.startClient();
expect(await syncEvents[0].promise).toBe(SyncState.Error); 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); 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); 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); 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); expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
}); });
it("should stop sync keep alive when client is stopped.", async () => { it("should stop sync keep alive when client is stopped.", async () => {
jest.useFakeTimers(); vi.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
fetchMock fetchMock
.get("end:capabilities", {}) .get("end:capabilities", {})
.getOnce("end:versions", {}) // first version check without credentials needs to succeed .getOnce("end:versions", {}) // first version check without credentials needs to succeed
@@ -146,9 +144,9 @@ describe("MatrixClient syncing errors", () => {
const syntState = await firstSyncEvent.promise; const syntState = await firstSyncEvent.promise;
expect(syntState).toBe(SyncState.Error); 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))); const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
+5 -4
View File
@@ -94,6 +94,7 @@ describe("MatrixClient syncing", () => {
presence: {}, presence: {},
}; };
// eslint-disable-next-line @vitest/expect-expect
it("should /sync after /pushrules and /filter.", async () => { it("should /sync after /pushrules and /filter.", async () => {
httpBackend!.when("GET", "/sync").respond(200, syncData); httpBackend!.when("GET", "/sync").respond(200, syncData);
@@ -501,7 +502,7 @@ describe("MatrixClient syncing", () => {
}) })
.respond(200, syncData); .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 }); client!.startClient({ initialSyncLimit: 1 });
return httpBackend!.flushAllExpected(); return httpBackend!.flushAllExpected();
@@ -994,7 +995,7 @@ describe("MatrixClient syncing", () => {
roomVersion: "org.matrix.msc2716v3", roomVersion: "org.matrix.msc2716v3",
}, },
].forEach((testMeta) => { ].forEach((testMeta) => {
// eslint-disable-next-line jest/valid-title // eslint-disable-next-line @vitest/valid-title
describe(testMeta.label, () => { describe(testMeta.label, () => {
const roomCreateEvent = utils.mkEvent({ const roomCreateEvent = utils.mkEvent({
type: "m.room.create", type: "m.room.create",
@@ -1835,7 +1836,7 @@ describe("MatrixClient syncing", () => {
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]); await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne); 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); 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" } }); const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
client!.store.storeAccountDataEvents([eventA1, eventB1]); client!.store.storeAccountDataEvents([eventA1, eventB1]);
const fn = jest.fn(); const fn = vi.fn();
client!.on(ClientEvent.AccountData, fn); client!.on(ClientEvent.AccountData, fn);
httpBackend!.when("GET", "/sync").respond(200, { httpBackend!.when("GET", "/sync").respond(200, {
@@ -62,7 +62,7 @@ describe("Notification count fixing", () => {
client!.startClient({ threadSupport: true }); client!.startClient({ threadSupport: true });
const room = new Room(roomId, client!, selfUserId); 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({ const event = new MatrixEvent({
room_id: roomId, room_id: roomId,
@@ -77,7 +77,7 @@ describe("Notification count fixing", () => {
}, },
}); });
jest.spyOn(event, "getPushActions").mockReturnValue({ vi.spyOn(event, "getPushActions").mockReturnValue({
notify: true, notify: true,
tweaks: {}, tweaks: {},
}); });
@@ -123,7 +123,7 @@ describe("MatrixClient syncing", () => {
]); ]);
const room = new Room(roomId, client!, selfUserId); 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 thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!; const threadReply = thread.events.at(-1)!;
@@ -143,7 +143,7 @@ describe("MatrixClient syncing", () => {
const reactionEventId = `$9-${Math.random()}-${Math.random()}`; const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
let lastEvent: MatrixEvent | null = null; 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; lastEvent = event as MatrixEvent;
return { event_id: reactionEventId }; return { event_id: reactionEventId };
}); });
@@ -195,7 +195,7 @@ describe("MatrixClient syncing", () => {
}) })
.respond(200, syncData); .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 }); client!.startClient({ initialSyncLimit: 1 });
await httpBackend!.flushAllExpected(); await httpBackend!.flushAllExpected();
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm"; import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
import { mocked } from "jest-mock"; import fetchMock from "@fetch-mock/vitest";
import fetchMock from "fetch-mock-jest";
import { import {
MSC4108FailureReason, MSC4108FailureReason,
@@ -40,7 +39,7 @@ import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient { function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
const baseUrl = "https://example.com"; const baseUrl = "https://example.com";
const crypto = { const crypto = {
exportSecretsForQrLogin: jest.fn(), exportSecretsForQrLogin: vi.fn(),
}; };
const client = { const client = {
doesServerSupportUnstableFeature(feature: string) { doesServerSupportUnstableFeature(feature: string) {
@@ -54,9 +53,9 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
}, },
baseUrl, baseUrl,
getDomain: () => "example.com", getDomain: () => "example.com",
getDevice: jest.fn(), getDevice: vi.fn(),
getCrypto: jest.fn(() => crypto), getCrypto: vi.fn(() => crypto),
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])), getAuthMetadata: vi.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
} as unknown as MatrixClient; } as unknown as MatrixClient;
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, { client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
baseUrl: client.baseUrl, baseUrl: client.baseUrl,
@@ -77,10 +76,6 @@ describe("MSC4108SignInWithQR", () => {
}); });
}); });
afterEach(() => {
fetchMock.reset();
});
const url = "https://fallbackserver/rz/123"; const url = "https://fallbackserver/rz/123";
const deviceId = "DEADB33F"; const deviceId = "DEADB33F";
const verificationUri = "https://example.com/verify"; const verificationUri = "https://example.com/verify";
@@ -115,10 +110,10 @@ describe("MSC4108SignInWithQR", () => {
let opponentData = Promise.withResolvers<string>(); let opponentData = Promise.withResolvers<string>();
const ourMockSession = { const ourMockSession = {
send: jest.fn(async (newData) => { send: vi.fn(async (newData) => {
ourData.resolve(newData); ourData.resolve(newData);
}), }),
receive: jest.fn(() => { receive: vi.fn(() => {
const prom = opponentData.promise; const prom = opponentData.promise;
prom.then(() => { prom.then(() => {
opponentData = Promise.withResolvers(); opponentData = Promise.withResolvers();
@@ -134,10 +129,10 @@ describe("MSC4108SignInWithQR", () => {
}, },
} as unknown as MSC4108RendezvousSession; } as unknown as MSC4108RendezvousSession;
const opponentMockSession = { const opponentMockSession = {
send: jest.fn(async (newData) => { send: vi.fn(async (newData) => {
opponentData.resolve(newData); opponentData.resolve(newData);
}), }),
receive: jest.fn(() => { receive: vi.fn(() => {
const prom = ourData.promise; const prom = ourData.promise;
prom.then(() => { prom.then(() => {
ourData = Promise.withResolvers(); ourData = Promise.withResolvers();
@@ -151,7 +146,7 @@ describe("MSC4108SignInWithQR", () => {
const ourChannel = new MSC4108SecureChannel(ourMockSession); const ourChannel = new MSC4108SecureChannel(ourMockSession);
const qrCodeData = QrCodeData.fromBytes( 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); 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 () => { it("should be able to connect with opponent and share verificationUri", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); 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([ await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({ expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
@@ -194,7 +189,7 @@ describe("MSC4108SignInWithQR", () => {
it("should abort if device already exists", async () => { it("should abort if device already exists", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
mocked(client.getDevice).mockResolvedValue({} as IMyDevice); vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
await Promise.all([ await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"), expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
@@ -244,12 +239,12 @@ describe("MSC4108SignInWithQR", () => {
// @ts-ignore // @ts-ignore
await opponentLogin.receive(); await opponentLogin.receive();
mocked(client.getDevice).mockResolvedValue({} as IMyDevice); vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
const secrets = { const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" }, 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 = { const payload = {
secrets: expect.objectContaining(secrets), secrets: expect.objectContaining(secrets),
@@ -261,13 +256,13 @@ describe("MSC4108SignInWithQR", () => {
}); });
it("should abort if device doesn't come up by timeout", async () => { 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(); fn();
// TODO: mock timers properly // TODO: mock timers properly
return -1 as any; return -1 as any;
}); });
jest.spyOn(Date, "now").mockImplementation(() => { vi.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + mocked(setTimeout).mock.calls.length * 1000; return 12345678 + vi.mocked(setTimeout).mock.calls.length * 1000;
}); });
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
@@ -280,7 +275,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.send({ await opponentLogin.send({
type: PayloadType.Success, 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(); const ourProm = ourLogin.shareSecrets();
await expect(ourProm).rejects.toThrow("New device not found"); await expect(ourProm).rejects.toThrow("New device not found");
@@ -297,7 +292,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.send({ await opponentLogin.send({
type: PayloadType.Success, type: PayloadType.Success,
}); });
mocked(client.getDevice).mockRejectedValue( vi.mocked(client.getDevice).mockRejectedValue(
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500), new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
); );
@@ -314,7 +309,7 @@ describe("MSC4108SignInWithQR", () => {
}); });
it("should not send secrets if user cancels", async () => { it("should not send secrets if user cancels", async () => {
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => { vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn(); fn();
// TODO: mock timers properly // TODO: mock timers properly
return -1 as any; return -1 as any;
@@ -334,7 +329,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.receive(); await opponentLogin.receive();
const deviceResolvers = Promise.withResolvers<IMyDevice>(); const deviceResolvers = Promise.withResolvers<IMyDevice>();
mocked(client.getDevice).mockReturnValue(deviceResolvers.promise); vi.mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {}); ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
deviceResolvers.resolve({} as IMyDevice); deviceResolvers.resolve({} as IMyDevice);
@@ -342,7 +337,7 @@ describe("MSC4108SignInWithQR", () => {
const secrets = { const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" }, 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([ await Promise.all([
expect(ourProm).rejects.toThrow("User cancelled"), expect(ourProm).rejects.toThrow("User cancelled"),
+21 -19
View File
@@ -26,7 +26,7 @@ import {
type Extension, type Extension,
} from "../../src/sliding-sync"; } from "../../src/sliding-sync";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { type IRoomEvent, type IStateEvent } from "../../src"; import { type IContent, type IRoomEvent, type IStateEvent } from "../../src";
import { import {
type MatrixClient, type MatrixClient,
type MatrixEvent, type MatrixEvent,
@@ -68,17 +68,17 @@ describe("SlidingSyncSdk", () => {
const selfAccessToken = "aseukfgwef"; const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => { const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getListParams = jest.fn(); s.getListParams = vi.fn();
s.getListData = jest.fn(); s.getListData = vi.fn();
s.getRoomSubscriptions = jest.fn(); s.getRoomSubscriptions = vi.fn();
s.modifyRoomSubscriptionInfo = jest.fn(); s.modifyRoomSubscriptionInfo = vi.fn();
s.modifyRoomSubscriptions = jest.fn(); s.modifyRoomSubscriptions = vi.fn();
s.registerExtension = jest.fn(); s.registerExtension = vi.fn();
s.setList = jest.fn(); s.setList = vi.fn();
s.setListRanges = jest.fn(); s.setListRanges = vi.fn();
s.start = jest.fn(); s.start = vi.fn();
s.stop = jest.fn(); s.stop = vi.fn();
s.resend = jest.fn(); s.resend = vi.fn();
return s; return s;
}; };
@@ -111,7 +111,7 @@ describe("SlidingSyncSdk", () => {
expect(m.getType()).toEqual(want[i].type); expect(m.getType()).toEqual(want[i].type);
expect(m.getSender()).toEqual(want[i].sender); expect(m.getSender()).toEqual(want[i].sender);
expect(m.getId()).toEqual(want[i].event_id); 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); expect(m.getTs()).toEqual(want[i].origin_server_ts);
if (want[i].unsigned) { if (want[i].unsigned) {
expect(m.getUnsigned()).toEqual(want[i].unsigned); expect(m.getUnsigned()).toEqual(want[i].unsigned);
@@ -150,7 +150,7 @@ describe("SlidingSyncSdk", () => {
// find an extension on a SlidingSyncSdk instance // find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension<any, any> => { const findExtension = (name: string): Extension<any, any> => {
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled(); expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync!.registerExtension as jest.Mock; const mockFn = vi.mocked(mockSlidingSync!.registerExtension);
// find the extension // find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) { for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>; const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
@@ -658,7 +658,7 @@ describe("SlidingSyncSdk", () => {
}); });
it("can update device lists", () => { it("can update device lists", () => {
syncCryptoCallback!.processDeviceLists = jest.fn(); syncCryptoCallback!.processDeviceLists = vi.fn();
ext.onResponse({ ext.onResponse({
device_lists: { device_lists: {
changed: ["@alice:localhost"], changed: ["@alice:localhost"],
@@ -672,7 +672,7 @@ describe("SlidingSyncSdk", () => {
}); });
it("can update OTK counts and unused fallback keys", () => { it("can update OTK counts and unused fallback keys", () => {
syncCryptoCallback!.processKeyCounts = jest.fn(); syncCryptoCallback!.processKeyCounts = vi.fn();
ext.onResponse({ ext.onResponse({
device_one_time_keys_count: { device_one_time_keys_count: {
signed_curve25519: 42, signed_curve25519: 42,
@@ -722,7 +722,7 @@ describe("SlidingSyncSdk", () => {
}); });
globalData = client!.getAccountData(globalType)!; globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeTruthy(); expect(globalData).toBeTruthy();
expect(globalData.getContent()).toEqual(globalContent); expect(globalData.getContent<IContent>()).toEqual(globalContent);
}); });
it("processes rooms account data", async () => { it("processes rooms account data", async () => {
@@ -757,7 +757,7 @@ describe("SlidingSyncSdk", () => {
expect(room).toBeTruthy(); expect(room).toBeTruthy();
const event = room.getAccountData(roomType)!; const event = room.getAccountData(roomType)!;
expect(event).toBeTruthy(); expect(event).toBeTruthy();
expect(event.getContent()).toEqual(roomContent); expect(event.getContent<IContent>()).toEqual(roomContent);
}); });
it("doesn't crash for unknown room account data", async () => { 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 () => { it("can handle missing fields", async () => {
ext.onResponse({ ext.onResponse({
next_batch: "23456", next_batch: "23456",
@@ -861,7 +862,7 @@ describe("SlidingSyncSdk", () => {
}; };
let called = false; let called = false;
client!.once(ClientEvent.ToDeviceEvent, (ev) => { client!.once(ClientEvent.ToDeviceEvent, (ev) => {
expect(ev.getContent()).toEqual(toDeviceContent); expect(ev.getContent<IContent>()).toEqual(toDeviceContent);
expect(ev.getType()).toEqual(toDeviceType); expect(ev.getType()).toEqual(toDeviceType);
called = true; called = true;
}); });
@@ -1095,6 +1096,7 @@ describe("SlidingSyncSdk", () => {
expect(receipt?.data.thread_id).toBeFalsy(); expect(receipt?.data.thread_id).toBeFalsy();
}); });
// eslint-disable-next-line @vitest/expect-expect
it("gracefully handles missing rooms when receiving receipts", async () => { it("gracefully handles missing rooms when receiving receipts", async () => {
const roomId = "!room:id"; const roomId = "!room:id";
const alice = "@alice:alice"; const alice = "@alice:alice";
+1
View File
@@ -82,6 +82,7 @@ describe("SlidingSync", () => {
await p; await p;
}); });
// eslint-disable-next-line @vitest/expect-expect
it("should stop the sync loop upon calling stop()", () => { it("should stop the sync loop upon calling stop()", () => {
slidingSync.stop(); slidingSync.stop();
httpBackend!.verifyNoOutstandingExpectation(); httpBackend!.verifyNoOutstandingExpectation();
+13 -4
View File
@@ -14,13 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
jest.mock("../src/http-api/utils", () => ({ import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
...jest.requireActual("../src/http-api/utils"),
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 // We mock timeoutSignal otherwise it causes tests to leave timers running
timeoutSignal: () => new AbortController().signal, 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) { 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. limitations under the License.
*/ */
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import { type ISyncResponder } from "./SyncResponder"; import { type ISyncResponder } from "./SyncResponder";
@@ -36,65 +36,80 @@ export class AccountDataAccumulator {
public constructor(private syncResponder: ISyncResponder) {} 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 * Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
* resolved. * 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. * @returns a Promise which will resolve (with the content of the account data) once it is set.
*/ */
public interceptSetAccountData( public interceptSetAccountData(): void {
accountDataType: string, if (this.setInterceptRunning) return;
opts?: Parameters<(typeof fetchMock)["put"]>[2], this.setInterceptRunning = true;
): 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);
// return a sync response fetchMock.put(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
this.sendSyncResponseWithUpdatedAccountData(); const content = JSON.parse(callLog.options.body as string);
return {}; const type = callLog.url.split("/").pop();
}, // update account data for sync response
opts, 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 * Intercept all requests to get account data
*/ */
public interceptGetAccountData(): void { public interceptGetAccountData(): void {
fetchMock.get( fetchMock.get(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
`express:/_matrix/client/v3/user/:userId/account_data/:type`, const type = callLog.url.split("/").pop();
(url) => { const existing = this.accountDataEvents.get(type!);
const type = url.split("/").pop(); if (existing) {
const existing = this.accountDataEvents.get(type!); // return it
if (existing) { return {
// return it status: 200,
return { body: existing,
status: 200, };
body: existing, } else {
}; // 404
} else { return {
// 404 status: 404,
return { body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
status: 404, };
body: { errcode: "M_NOT_FOUND", error: "Account data not found." }, }
}; });
}
},
{ overwriteRoutes: true },
);
} }
/** /**
+10 -9
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/ */
import debugFunc, { type Debugger } from "debug"; 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 { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
import type { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src"; import type { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src";
@@ -81,26 +81,27 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
// set up a listener for /keys/upload. // set up a listener for /keys/upload.
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => { this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
const listener = (url: string, options: RequestInit) => fetchMock.post(
this.onKeyUploadRequest(resolveOneTimeKeys, options); new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(),
(callLog) => this.onKeyUploadRequest(resolveOneTimeKeys, callLog.options),
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener); { name: routeNamePrefix + "keys-upload" },
);
}); });
fetchMock.post( 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", name: routeNamePrefix + "upload-sigs",
}, },
(url, options) => this.onSignaturesUploadRequest(options),
); );
fetchMock.post( 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", 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. limitations under the License.
*/ */
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import { MapWithDefault } from "../../src/utils"; import { MapWithDefault } from "../../src/utils";
import { type IDownloadKeyResult, type SigningKeys } from "../../src"; import { type IDownloadKeyResult, type SigningKeys } from "../../src";
@@ -42,8 +42,9 @@ export class E2EKeyResponder {
*/ */
public constructor(homeserverUrl: string) { public constructor(homeserverUrl: string) {
// set up a listener for /keys/query. // 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(), (callLog) =>
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener); this.onKeyQueryRequest(callLog.options),
);
} }
private onKeyQueryRequest(options: RequestInit) { 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. limitations under the License.
*/ */
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import { MapWithDefault } from "../../src/utils"; import { MapWithDefault } from "../../src/utils";
import { type E2EKeyReceiver } from "./E2EKeyReceiver"; import { type E2EKeyReceiver } from "./E2EKeyReceiver";
@@ -36,8 +36,9 @@ export class E2EOTKClaimResponder {
* @param homeserverUrl - the Homeserver Url of the client under test. * @param homeserverUrl - the Homeserver Url of the client under test.
*/ */
public constructor(homeserverUrl: string) { public constructor(homeserverUrl: string) {
const listener = (url: string, options: RequestInit) => this.onKeyClaimRequest(options); fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), (callLog) =>
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), listener); this.onKeyClaimRequest(callLog.options),
);
} }
private onKeyClaimRequest(options: RequestInit) { private onKeyClaimRequest(options: RequestInit) {
+4 -5
View File
@@ -16,9 +16,8 @@ limitations under the License.
import debugFunc from "debug"; import debugFunc from "debug";
import { type Debugger } from "debug"; import { type Debugger } from "debug";
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
import type FetchMock from "fetch-mock";
/** Interface implemented by classes that intercept `/sync` requests from test clients /** Interface implemented by classes that intercept `/sync` requests from test clients
* *
@@ -76,12 +75,12 @@ export class SyncResponder implements ISyncResponder {
*/ */
public constructor(homeserverUrl: string) { public constructor(homeserverUrl: string) {
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`); 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(), this.onSyncRequest(),
); );
} }
private async onSyncRequest(): Promise<FetchMock.MockResponse> { private async onSyncRequest(): Promise<RouteResponse> {
switch (this.state) { switch (this.state) {
case SyncResponderState.IDLE: { case SyncResponderState.IDLE: {
this.debug("Got /sync request: waiting for response to be ready"); 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. 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 { type ClientEventHandlerMap, type EmittedEvents, type MatrixClient } from "../../src/client";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { User } from "../../src/models/user"; 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 * Mock client with real event emitter
* useful for testing code that listens * useful for testing code that listens
@@ -34,19 +40,19 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
/** /**
* - make a mock client * - 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 * - spy on MatrixClientPeg.get to return the mock
* eg * eg
* ``` * ```
* const mockClient = getMockClientWithEventEmitter({ * const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId), getUserId: vi.fn().mockReturnValue(aliceId),
}); });
* ``` * ```
*/ */
export const getMockClientWithEventEmitter = ( export const getMockClientWithEventEmitter = (
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>, mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
): MockedObject<MatrixClient> => { ): 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; return mock;
}; };
@@ -59,14 +65,14 @@ export const getMockClientWithEventEmitter = (
* ``` * ```
*/ */
export const mockClientMethodsUser = (userId = "@alice:domain") => ({ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
getUserId: jest.fn().mockReturnValue(userId), getUserId: vi.fn().mockReturnValue(userId),
getSafeUserId: jest.fn().mockReturnValue(userId), getSafeUserId: vi.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)), getUser: vi.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false), isGuest: vi.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), mxcUrlToHttp: vi.fn().mockReturnValue("mock-mxcUrlToHttp"),
credentials: { userId }, credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }), getThreePids: vi.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(), getAccessToken: vi.fn(),
}); });
/** /**
@@ -78,16 +84,16 @@ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
* ``` * ```
*/ */
export const mockClientMethodsEvents = () => ({ export const mockClientMethodsEvents = () => ({
decryptEventIfNeeded: jest.fn(), decryptEventIfNeeded: vi.fn(),
getPushActionsForEvent: jest.fn(), getPushActionsForEvent: vi.fn(),
}); });
/** /**
* Returns basic mocked client methods related to server support * Returns basic mocked client methods related to server support
*/ */
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({ export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getIdentityServerUrl: jest.fn(), getIdentityServerUrl: vi.fn(),
getHomeserverUrl: jest.fn(), getHomeserverUrl: vi.fn(),
getCachedCapabilities: jest.fn().mockReturnValue({}), getCachedCapabilities: vi.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false), 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. limitations under the License.
*/ */
import { type MockInstance } from "vitest";
/** /**
* Filter emitter.emit mock calls to find relevant events * Filter emitter.emit mock calls to find relevant events
* eg: * eg:
* ``` * ```
* const emitSpy = jest.spyOn(state, 'emit'); * const emitSpy = vi.spyOn(state, 'emit');
* << actions >> * << actions >>
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy); * const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
* expect(beaconLivenessEmits.length).toBe(1); * 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); 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. limitations under the License.
*/ */
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of // Vitest lacks tickAsync() and a number of other async methods which break the event loop,
// other async methods which break the event loop, letting scheduled promise // letting scheduled promise callbacks run. So we have to do it manually
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do // (this is what sinon does under the hood). We do both in a loop until the thing we expect happens:
// it manually (this is what sinon does under the hood). We do both in a loop // hopefully this is the least flakey way and avoids assuming anything about the app's behaviour.
// 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; const realSetTimeout = setTimeout;
export function flushPromises() { export function flushPromises() {
return new Promise((r) => { 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. limitations under the License.
*/ */
import fetchMock from "fetch-mock-jest"; import fetchMock from "@fetch-mock/vitest";
import { type KeyBackupInfo } from "../../src/crypto-api"; 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`. * @param userId - the local user's ID. Defaults to `@alice:localhost`.
*/ */
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") { export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
fetchMock.getOnce( fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
new URL("/_matrix/client/versions", homeserverUrl).toString(), fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
{ versions: ["v1.1"] },
{ overwriteRoutes: true },
);
fetchMock.getOnce(
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
{},
{ overwriteRoutes: true },
);
fetchMock.postOnce( fetchMock.postOnce(
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(), new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(),
{ filter_id: "fid" }, { 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`. * @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
*/ */
export function mockSetupMegolmBackupRequests(backupVersion: string): void { export function mockSetupMegolmBackupRequests(backupVersion: string): void {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", { fetchMock.get(
status: 404, "path:/_matrix/client/v3/room_keys/version",
body: { {
errcode: "M_NOT_FOUND", status: 404,
error: "No current backup version", 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) => { fetchMock.post(
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}"); "path:/_matrix/client/v3/room_keys/version",
backupData.version = backupVersion; (callLog) => {
backupData.count = 0; const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
backupData.etag = "zer"; backupData.version = backupVersion;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, { backupData.count = 0;
overwriteRoutes: true, backupData.etag = "zer";
}); fetchMock.modifyRoute("room-keys-version", { response: backupData });
return { return {
version: backupVersion, 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` * Do not edit by hand! This file is generated by `./generate-test-data.py`
*/ */
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto"; import type {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
import {{ IDownloadKeyResult, IEvent }} from "../../../src"; import type {{ IDownloadKeyResult, IEvent }} from "../../../src";
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup"; import type {{ KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions }} from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */ /* 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` */ /** Signed OTKs, returned by `POST /keys/claim` */
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) }; 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 */ /** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = { export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
json.dumps(set_of_exported_room_keys, indent=4) 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 */ /** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)}; 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") 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]: 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 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) Returns the exported key, the matching privat edKey (needed to encrypt)
""" """
index = 0 index = 0
@@ -409,11 +417,12 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
"session_id": encode_base64( "session_id": encode_base64(
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) 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": { "sender_claimed_keys": {
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)), "ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
}, },
"forwarding_curve25519_key_chain": [], "forwarding_curve25519_key_chain": [],
"m.shared_history": True,
} }
return megolm_export, private_key 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", "room_id": "!room:id",
"sender_key": previous["sender_key"], "sender_key": previous["sender_key"],
"session_id": previous["session_id"], "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"], "sender_claimed_keys": previous["sender_claimed_keys"],
"forwarding_curve25519_key_chain": [], "forwarding_curve25519_key_chain": [],
} }
@@ -609,7 +618,7 @@ def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519
message += signature message += signature
cipher_text = encode_base64(message) cipher_text = encode_base64(bytes(message))
encrypted_payload = { encrypted_payload = {
"algorithm" : "m.megolm.v1.aes-sha2", "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') export_bytes += parity_byte.to_bytes(1, 'big')
# The byte string is encoded using base58 # 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)] split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
return ' '.join(split) 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` * 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 { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
import { type IDownloadKeyResult, type IEvent } from "../../../src"; import type { IDownloadKeyResult, IEvent } from "../../../src";
import { type KeyBackupSession, type KeyBackupInfo } from "../../../src/crypto-api/keybackup"; import type { KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions } from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */ /* 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 */ /** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
{ {
@@ -149,7 +129,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": { "sender_claimed_keys": {
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0" "ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
}, },
"forwarding_curve25519_key_chain": [] "forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
}, },
{ {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
@@ -160,7 +141,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": { "sender_claimed_keys": {
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog" "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": { "sender_claimed_keys": {
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI" "ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
}, },
"forwarding_curve25519_key_chain": [] "forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
}; };
/** A ratcheted version of MEGOLM_SESSION_DATA */ /** A ratcheted version of MEGOLM_SESSION_DATA */
@@ -196,7 +179,7 @@ export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"forwarded_count": 0, "forwarded_count": 0,
"is_verified": false, "is_verified": false,
"session_data": { "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", "ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
"mac": "OibmACbORhI" "mac": "OibmACbORhI"
} }
@@ -229,6 +212,37 @@ export const ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000 "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 // Bob data
export const BOB_TEST_USER_ID = "@bob:xyz"; 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 */ /** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
{ {
@@ -369,7 +363,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": { "sender_claimed_keys": {
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA" "ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
}, },
"forwarding_curve25519_key_chain": [] "forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": true
}, },
{ {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
@@ -380,7 +375,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
"sender_claimed_keys": { "sender_claimed_keys": {
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM" "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": { "sender_claimed_keys": {
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64" "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 */ /** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
@@ -416,7 +413,7 @@ export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"forwarded_count": 0, "forwarded_count": 0,
"is_verified": false, "is_verified": false,
"session_data": { "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", "ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
"mac": "lEfHlqfJQwU" "mac": "lEfHlqfJQwU"
} }
@@ -449,6 +446,34 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
"origin_server_ts": 1507753886000 "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 */ /** 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> = { export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": { "master_keys": {
+28 -9
View File
@@ -13,6 +13,7 @@ import {
import { import {
ClientEvent, ClientEvent,
EventType, EventType,
HistoryVisibility,
type IJoinedRoom, type IJoinedRoom,
type IPusher, type IPusher,
type ISyncResponse, type ISyncResponse,
@@ -57,13 +58,22 @@ 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 * Return a sync response which contains a single room (by default `TEST_ROOM_ID`), with the members given
* @param roomMembers * and history visibility set to `shared`.
* @param roomId
* *
* @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[], roomId = TEST_ROOM_ID): ISyncResponse { export function getSyncResponse(
roomMembers: string[],
roomHistoryVisibility: HistoryVisibility = HistoryVisibility.Shared,
roomId = TEST_ROOM_ID,
encryptStateEvents = false,
): ISyncResponse {
const roomResponse: IJoinedRoom = { const roomResponse: IJoinedRoom = {
summary: { summary: {
"m.heroes": [], "m.heroes": [],
@@ -77,7 +87,16 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
type: "m.room.encryption", type: "m.room.encryption",
state_key: "", state_key: "",
content: { content: {
algorithm: "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
"io.element.msc4362.encrypt_state_events": encryptStateEvents,
},
}),
mkEventCustom({
sender: roomMembers[0],
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: roomHistoryVisibility,
}, },
}), }),
], ],
@@ -131,7 +150,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
// eslint-disable-line guard-for-in // eslint-disable-line guard-for-in
try { try {
if (constr.prototype[key] instanceof Function) { if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn(); result[key] = vi.fn();
} }
} catch { } catch {
// Direct access to some non-function fields of DOM prototypes may // Direct access to some non-function fields of DOM prototypes may
@@ -587,7 +606,7 @@ export async function advanceTimersUntil<T>(promise: Promise<T>): Promise<T> {
}); });
while (!resolved) { while (!resolved) {
await jest.advanceTimersByTimeAsync(1); await vi.advanceTimersByTimeAsync(1);
} }
return await promise; return await promise;
@@ -636,7 +655,7 @@ export function waitFor<T>(
checkCallback(); checkCallback();
while (!finished) { while (!finished) {
jest.advanceTimersByTime(interval); vi.advanceTimersByTime(interval);
// Could have timed-out // Could have timed-out
if (finished) break; if (finished) break;
+51 -92
View File
@@ -17,21 +17,17 @@ limitations under the License.
import { import {
type ClientEvent, type ClientEvent,
type ClientEventHandlerMap, type ClientEventHandlerMap,
type EmptyObject,
EventType, EventType,
type GroupCall, type GroupCall,
GroupCallIntent, GroupCallIntent,
GroupCallType, GroupCallType,
type IContent, type IContent,
type ISendEventResponse,
type MatrixClient, type MatrixClient,
type MatrixEvent, type MatrixEvent,
type Room,
RoomMember, RoomMember,
type RoomState, type RoomState,
RoomStateEvent, RoomStateEvent,
type RoomStateEventHandlerMap, type RoomStateEventHandlerMap,
type SendToDeviceContentMap,
} from "../../src"; } from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter"; import { ReEmitter } from "../../src/ReEmitter";
@@ -269,19 +265,20 @@ export class MockRTCRtpTransceiver {
this.peerConn.needsNegotiation = true; 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( constructor(
public readonly id: string, public readonly id: string,
public readonly kind: "audio" | "video", public readonly kind: "audio" | "video",
public enabled = true, public enabled = true,
) {} ) {
super();
}
public stop = jest.fn<void, []>(); public stop = vi.fn<() => void>();
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false; public isStopped = false;
public settings?: MediaTrackSettings; public settings?: MediaTrackSettings;
@@ -289,45 +286,21 @@ export class MockMediaStreamTrack {
return this.settings!; 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 { public typed(): MediaStreamTrack {
return this as unknown as MediaStreamTrack; return this as unknown as MediaStreamTrack;
} }
} }
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own export class MockMediaStream extends EventTarget {
// implementation
export class MockMediaStream {
constructor( constructor(
public id: string, public id: string,
private tracks: MockMediaStreamTrack[] = [], private tracks: MockMediaStreamTrack[] = [],
) {} ) {
super();
}
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false; public isStopped = false;
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public getTracks() { public getTracks() {
return this.tracks; return this.tracks;
} }
@@ -337,17 +310,9 @@ export class MockMediaStream {
public getVideoTracks() { public getVideoTracks() {
return this.tracks.filter((track) => track.kind === "video"); 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) { public addTrack(track: MockMediaStreamTrack) {
this.tracks.push(track); this.tracks.push(track);
this.dispatchEvent("addtrack"); this.dispatchEvent(new Event("addtrack"));
} }
public removeTrack(track: MockMediaStreamTrack) { public removeTrack(track: MockMediaStreamTrack) {
this.tracks.splice(this.tracks.indexOf(track), 1); this.tracks.splice(this.tracks.indexOf(track), 1);
@@ -391,7 +356,7 @@ export class MockMediaHandler {
public stopUserMediaStream(stream: MockMediaStream) { public stopUserMediaStream(stream: MockMediaStream) {
stream.isStopped = true; stream.isStopped = true;
} }
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => { public getScreensharingStream = vi.fn((opts?: IScreensharingOpts) => {
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")]; const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio")); if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
@@ -416,19 +381,19 @@ export class MockMediaHandler {
} }
export class MockMediaDevices { export class MockMediaDevices {
public enumerateDevices = jest public enumerateDevices = vi
.fn<Promise<MediaDeviceInfo[]>, []>() .fn<MediaDevices["enumerateDevices"]>()
.mockResolvedValue([ .mockResolvedValue([
new MockMediaDeviceInfo("audioinput").typed(), new MockMediaDeviceInfo("audioinput").typed(),
new MockMediaDeviceInfo("videoinput").typed(), new MockMediaDeviceInfo("videoinput").typed(),
]); ]);
public getUserMedia = jest public getUserMedia = vi
.fn<Promise<MediaStream>, [MediaStreamConstraints]>() .fn<MediaDevices["getUserMedia"]>()
.mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed())); .mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
public getDisplayMedia = jest public getDisplayMedia = vi
.fn<Promise<MediaStream>, [MediaStreamConstraints]>() .fn<MediaDevices["getDisplayMedia"]>()
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed())); .mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
public typed(): MediaDevices { public typed(): MediaDevices {
@@ -462,14 +427,8 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
calls: new Map<string, MatrixCall>(), calls: new Map<string, MatrixCall>(),
}; };
public sendStateEvent = jest.fn< public sendStateEvent = vi.fn<MatrixClient["sendStateEvent"]>();
Promise<ISendEventResponse>, public sendToDevice = vi.fn<MatrixClient["sendToDevice"]>();
[roomId: string, eventType: EventType, content: any, statekey: string]
>();
public sendToDevice = jest.fn<
Promise<EmptyObject>,
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
>();
public isInitialSyncComplete(): boolean { public isInitialSyncComplete(): boolean {
return false; return false;
@@ -499,11 +458,11 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getUseE2eForGroupCall = () => false; public getUseE2eForGroupCall = () => false;
public checkTurnServers = () => null; 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 getRooms = vi.fn<MatrixClient["getRooms"]>().mockReturnValue([]);
public getRoom = jest.fn(); public getRoom = vi.fn();
public getFoci = jest.fn(); public getFoci = vi.fn();
public supportsThreads(): boolean { public supportsThreads(): boolean {
return true; return true;
@@ -534,20 +493,20 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
public opponentMember = { userId: this.opponentUserId }; public opponentMember = { userId: this.opponentUserId };
public callId = "1"; public callId = "1";
public localUsermediaFeed = { public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(), setAudioVideoMuted: vi.fn<CallFeed["setAudioVideoMuted"]>(),
isAudioMuted: jest.fn().mockReturnValue(false), isAudioMuted: vi.fn().mockReturnValue(false),
isVideoMuted: jest.fn().mockReturnValue(false), isVideoMuted: vi.fn().mockReturnValue(false),
stream: new MockMediaStream("stream"), stream: new MockMediaStream("stream"),
} as unknown as CallFeed; } as unknown as CallFeed;
public remoteUsermediaFeed?: CallFeed; public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed; public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>(); public reject = vi.fn<() => void>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>(); public answerWithCallFeeds = vi.fn<MatrixCall["answerWithCallFeeds"]>();
public hangup = jest.fn<void, []>(); public hangup = vi.fn<() => void>();
public initStats = jest.fn<void, []>(); public initStats = vi.fn<() => void>();
public sendMetadataUpdate = jest.fn<void, []>(); public sendMetadataUpdate = vi.fn<() => void>();
public getOpponentMember(): Partial<RoomMember> { public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember; return this.opponentMember;
@@ -586,11 +545,11 @@ export class MockCallFeed {
} }
export function installWebRTCMocks() { export function installWebRTCMocks() {
globalThis.navigator = { vi.stubGlobal("navigator", {
mediaDevices: new MockMediaDevices().typed(), mediaDevices: new MockMediaDevices().typed(),
} as unknown as Navigator; });
globalThis.window = { vi.stubGlobal("window", {
// @ts-ignore Mock // @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection, RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock // @ts-ignore Mock
@@ -598,16 +557,16 @@ export function installWebRTCMocks() {
// @ts-ignore Mock // @ts-ignore Mock
RTCIceCandidate: {}, RTCIceCandidate: {},
getUserMedia: () => new MockMediaStream("local_stream"), getUserMedia: () => new MockMediaStream("local_stream"),
}; });
// @ts-ignore Mock
globalThis.document = {}; vi.stubGlobal("document", {});
// @ts-ignore Mock // @ts-ignore Mock
globalThis.AudioContext = MockAudioContext; globalThis.AudioContext = MockAudioContext;
// @ts-ignore Mock // @ts-ignore Mock
globalThis.RTCRtpReceiver = { globalThis.RTCRtpReceiver = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({ getCapabilities: vi.fn().mockReturnValue({
codecs: [], codecs: [],
headerExtensions: [], headerExtensions: [],
}), }),
@@ -615,7 +574,7 @@ export function installWebRTCMocks() {
// @ts-ignore Mock // @ts-ignore Mock
globalThis.RTCRtpSender = { globalThis.RTCRtpSender = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({ getCapabilities: vi.fn().mockReturnValue({
codecs: [], codecs: [],
headerExtensions: [], headerExtensions: [],
}), }),
@@ -632,22 +591,22 @@ export function makeMockGroupCallStateEvent(
redacted?: boolean, redacted?: boolean,
): MatrixEvent { ): MatrixEvent {
return { return {
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix), getType: vi.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: jest.fn().mockReturnValue(roomId), getRoomId: vi.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0), getTs: vi.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue(content), getContent: vi.fn().mockReturnValue(content),
getStateKey: jest.fn().mockReturnValue(groupCallId), getStateKey: vi.fn().mockReturnValue(groupCallId),
isRedacted: jest.fn().mockReturnValue(redacted ?? false), isRedacted: vi.fn().mockReturnValue(redacted ?? false),
} as unknown as MatrixEvent; } as unknown as MatrixEvent;
} }
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent { export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
return { return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getRoomId: jest.fn().mockReturnValue(roomId), getRoomId: vi.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0), getTs: vi.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue({}), getContent: vi.fn().mockReturnValue({}),
getStateKey: jest.fn().mockReturnValue(groupCallId), getStateKey: vi.fn().mockReturnValue(groupCallId),
} as unknown as MatrixEvent; } as unknown as MatrixEvent;
} }
+2 -2
View File
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
}); });
it("should have a falsey unstable if needed", () => { 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.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy(); expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.stable]); expect(ns.names).toEqual([ns.stable]);
@@ -61,7 +61,7 @@ describe("UnstableValue", () => {
it("should return unstable if there is no stable", () => { it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null!, "unstable"); const ns = new UnstableValue(null!, "unstable");
expect(ns.name).toBe(ns.unstable); expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy(); expect(<any>ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]); expect(ns.names).toEqual([ns.unstable]);
}); });
+2 -2
View File
@@ -38,7 +38,7 @@ describe("ReEmitter", function () {
const src = new EventSource(); const src = new EventSource();
const tgt = new EventTarget(); const tgt = new EventTarget();
const handler = jest.fn(); const handler = vi.fn();
tgt.on(EVENTNAME, handler); tgt.on(EVENTNAME, handler);
const reEmitter = new ReEmitter(tgt); const reEmitter = new ReEmitter(tgt);
@@ -61,7 +61,7 @@ describe("ReEmitter", function () {
// without the workaround in ReEmitter, this would throw // without the workaround in ReEmitter, this would throw
src.doAnError(); src.doAnError();
const handler = jest.fn(); const handler = vi.fn();
tgt.on("error", handler); tgt.on("error", handler);
src.doAnError(); src.doAnError();
+3 -3
View File
@@ -35,16 +35,16 @@ describe("onResumedSync", () => {
}; };
store = new StubStore(); store = new StubStore();
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => { store.getOldestToDeviceBatch = vi.fn().mockImplementation(() => {
return batch; return batch;
}); });
store.removeToDeviceBatch = jest.fn().mockImplementation(() => { store.removeToDeviceBatch = vi.fn().mockImplementation(() => {
batch = null; batch = null;
}); });
mockClient = getMockClientWithEventEmitter({}); mockClient = getMockClientWithEventEmitter({});
mockClient.store = store; mockClient.store = store;
mockClient.sendToDevice = jest.fn().mockImplementation(async () => { mockClient.sendToDevice = vi.fn().mockImplementation(async () => {
if (shouldFailSendToDevice) { if (shouldFailSendToDevice) {
await Promise.reject(new ConnectionError("")).finally(() => { await Promise.reject(new ConnectionError("")).finally(() => {
setTimeout(onSendToDeviceFailure, 0); setTimeout(onSendToDeviceFailure, 0);
+2 -1
View File
@@ -35,6 +35,7 @@ describe("AutoDiscovery", function () {
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch); AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
}); });
// eslint-disable-next-line @vitest/expect-expect
it("should throw an error when no domain is specified", function () { it("should throw an error when no domain is specified", function () {
getHttpBackend(); getHttpBackend();
return Promise.all([ return Promise.all([
@@ -190,7 +191,7 @@ describe("AutoDiscovery", function () {
}; };
return Promise.all([ return Promise.all([
httpBackend.flushAllExpected(), httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then(expect(expected).toEqual), AutoDiscovery.findClientConfig("example.org").then((config) => expect(config).toEqual(expected)),
]); ]);
}); });
+72 -39
View File
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
describe("makeBeaconInfoContent()", () => { describe("makeBeaconInfoContent()", () => {
const mockDateNow = 123456789; const mockDateNow = 123456789;
beforeEach(() => { beforeEach(() => {
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow); vi.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
}); });
afterAll(() => { afterAll(() => {
jest.spyOn(globalThis.Date, "now").mockRestore(); vi.spyOn(globalThis.Date, "now").mockRestore();
}); });
it("create fully defined event content", () => { it("create fully defined event content", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({ expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
@@ -183,39 +183,43 @@ describe("Topic content helpers", () => {
it("creates fully defined event content without html", () => { it("creates fully defined event content without html", () => {
expect(makeTopicContent("pizza")).toEqual({ expect(makeTopicContent("pizza")).toEqual({
topic: "pizza", topic: "pizza",
[M_TOPIC.name]: [ [M_TOPIC.name]: {
{ "m.text": [
body: "pizza", {
mimetype: "text/plain", body: "pizza",
}, mimetype: "text/plain",
], },
],
},
}); });
}); });
it("creates fully defined event content with html", () => { it("creates fully defined event content with html", () => {
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({ expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
topic: "pizza", topic: "pizza",
[M_TOPIC.name]: [ [M_TOPIC.name]: {
{ "m.text": [
body: "<b>pizza</b>", {
mimetype: "text/html", body: "<b>pizza</b>",
}, mimetype: "text/html",
{ },
body: "pizza", {
mimetype: "text/plain", body: "pizza",
}, mimetype: "text/plain",
], },
],
},
}); });
}); });
it("creates an empty event when the topic is falsey", () => { it("creates an empty event when the topic is falsey", () => {
expect(makeTopicContent(undefined)).toEqual({ expect(makeTopicContent(undefined)).toEqual({
topic: undefined, topic: undefined,
[M_TOPIC.name]: [], [M_TOPIC.name]: { "m.text": [] },
}); });
expect(makeTopicContent(null)).toEqual({ expect(makeTopicContent(null)).toEqual({
topic: null, topic: null,
[M_TOPIC.name]: [], [M_TOPIC.name]: { "m.text": [] },
}); });
}); });
}); });
@@ -225,11 +229,13 @@ describe("Topic content helpers", () => {
expect( expect(
parseTopicContent({ parseTopicContent({
topic: "pizza", topic: "pizza",
[M_TOPIC.name]: [ [M_TOPIC.name]: {
{ "m.text": [
body: "pizza", {
}, body: "pizza",
], },
],
},
}), }),
).toEqual({ ).toEqual({
text: "pizza", text: "pizza",
@@ -240,12 +246,14 @@ describe("Topic content helpers", () => {
expect( expect(
parseTopicContent({ parseTopicContent({
topic: "pizza", topic: "pizza",
[M_TOPIC.name]: [ [M_TOPIC.name]: {
{ "m.text": [
body: "pizza", {
mimetype: "text/plain", body: "pizza",
}, mimetype: "text/plain",
], },
],
},
}), }),
).toEqual({ ).toEqual({
text: "pizza", text: "pizza",
@@ -256,12 +264,14 @@ describe("Topic content helpers", () => {
expect( expect(
parseTopicContent({ parseTopicContent({
topic: "pizza", topic: "pizza",
[M_TOPIC.name]: [ [M_TOPIC.name]: {
{ "m.text": [
body: "<b>pizza</b>", {
mimetype: "text/html", body: "<b>pizza</b>",
}, mimetype: "text/html",
], },
],
},
}), }),
).toEqual({ ).toEqual({
text: "pizza", text: "pizza",
@@ -279,11 +289,34 @@ describe("Topic content helpers", () => {
}); });
}); });
it("uses legacy event content when new topic key is invalid", () => { // TODO delete this test and re-enable the next one after support for the invalid form is removed
// https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
it("parses malformed event content with html topic", () => {
expect( expect(
parseTopicContent({ parseTopicContent({
"topic": "pizza", "topic": "pizza",
"m.topic": {} as any, "m.topic": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
] as any,
}),
).toEqual({
text: "pizza",
html: "<b>pizza</b>",
});
});
it.skip("uses legacy event content when new topic key is invalid", () => {
expect(
parseTopicContent({
"topic": "pizza",
"m.topic": [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
] as any,
}), }),
).toEqual({ ).toEqual({
text: "pizza", text: "pizza",
+4 -1
View File
@@ -14,8 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/**
* @vitest-environment happy-dom
*/
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import "jest-localstorage-mock";
import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src"; import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src";
import { type CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base"; 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 () => { it("throws if webcrypto is not available", async () => {
const oldCrypto = globalThis.crypto; vi.stubGlobal("crypto", {});
try { await expect(sha256("test")).rejects.toThrow();
globalThis.crypto = {} as any;
await expect(sha256("test")).rejects.toThrow();
} finally {
globalThis.crypto = oldCrypto;
}
}); });
}); });
+268 -51
View File
@@ -1,7 +1,3 @@
/**
* @jest-environment jsdom
*/
/* /*
Copyright 2022 The Matrix.org Foundation C.I.C. 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 // project, which doesn't know about our TypeEventEmitter implementation at all
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { type MockedObject } from "jest-mock"; import { type MockedObject } from "vitest";
import { import {
type WidgetApi, type WidgetApi,
WidgetApiToWidgetAction, WidgetApiToWidgetAction,
@@ -36,7 +32,14 @@ import {
type IRoomEvent, type IRoomEvent,
} from "matrix-widget-api"; } 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 { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync"; import { SyncState } from "../../src/sync";
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded"; import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
@@ -46,6 +49,7 @@ import { sleep } from "../../src/utils";
import { SlidingSync } from "../../src/sliding-sync"; import { SlidingSync } from "../../src/sliding-sync";
import { logger } from "../../src/logger"; import { logger } from "../../src/logger";
import { flushPromises } from "../test-utils/flushPromises"; import { flushPromises } from "../test-utils/flushPromises";
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
const testOIDCToken = { const testOIDCToken = {
access_token: "12345678", access_token: "12345678",
@@ -53,27 +57,28 @@ const testOIDCToken = {
matrix_server_name: "homeserver.oabc", matrix_server_name: "homeserver.oabc",
token_type: "Bearer", token_type: "Bearer",
}; };
class MockWidgetApi extends EventEmitter { class MockWidgetApi extends EventEmitter {
public start = jest.fn().mockResolvedValue(undefined); public start = vi.fn().mockResolvedValue(undefined);
public getClientVersions = jest.fn(); public getClientVersions = vi.fn();
public requestCapability = jest.fn().mockResolvedValue(undefined); public requestCapability = vi.fn().mockResolvedValue(undefined);
public requestCapabilities = jest.fn().mockResolvedValue(undefined); public requestCapabilities = vi.fn().mockResolvedValue(undefined);
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined); public requestCapabilityForRoomTimeline = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendEvent = jest.fn().mockResolvedValue(undefined); public requestCapabilityToSendEvent = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveEvent = jest.fn().mockResolvedValue(undefined); public requestCapabilityToReceiveEvent = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendMessage = jest.fn().mockResolvedValue(undefined); public requestCapabilityToSendMessage = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveMessage = jest.fn().mockResolvedValue(undefined); public requestCapabilityToReceiveMessage = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendState = jest.fn().mockResolvedValue(undefined); public requestCapabilityToSendState = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveState = jest.fn().mockResolvedValue(undefined); public requestCapabilityToReceiveState = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendToDevice = jest.fn().mockResolvedValue(undefined); public requestCapabilityToSendToDevice = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveToDevice = jest.fn().mockResolvedValue(undefined); public requestCapabilityToReceiveToDevice = vi.fn().mockResolvedValue(undefined);
public sendRoomEvent = jest.fn( public sendRoomEvent = vi.fn(
async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) => async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
delay === undefined && parentDelayId === undefined delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` } ? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` }, : { delay_id: `id-${Math.random()}` },
); );
public sendStateEvent = jest.fn( public sendStateEvent = vi.fn(
async ( async (
eventType: string, eventType: string,
stateKey: string, stateKey: string,
@@ -86,22 +91,24 @@ class MockWidgetApi extends EventEmitter {
? { event_id: `$${Math.random()}` } ? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` }, : { delay_id: `id-${Math.random()}` },
); );
public updateDelayedEvent = jest.fn().mockResolvedValue(undefined); public cancelScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
public sendToDevice = jest.fn().mockResolvedValue(undefined); public restartScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
public requestOpenIDConnectToken = jest.fn(async () => { public sendScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
public sendToDevice = vi.fn().mockResolvedValue(undefined);
public requestOpenIDConnectToken = vi.fn(async () => {
return testOIDCToken; return testOIDCToken;
return new Promise<IOpenIDCredentials>(() => { return new Promise<IOpenIDCredentials>(() => {
return testOIDCToken; return testOIDCToken;
}); });
}); });
public readStateEvents = jest.fn(async () => []); public readStateEvents = vi.fn(async () => []);
public getTurnServers = jest.fn(async () => []); public getTurnServers = vi.fn(async () => []);
public sendContentLoaded = jest.fn().mockResolvedValue(undefined); public sendContentLoaded = vi.fn().mockResolvedValue(undefined);
public transport = { public transport = {
reply: jest.fn(), reply: vi.fn(),
send: jest.fn(), send: vi.fn(),
sendComplete: jest.fn(), sendComplete: vi.fn(),
}; };
/** /**
@@ -169,6 +176,9 @@ describe("RoomWidgetClient", () => {
"org.matrix.rageshake_request", "org.matrix.rageshake_request",
{ request_id: 123 }, { request_id: 123 },
"!1:example.org", "!1:example.org",
undefined,
undefined,
undefined,
); );
}); });
@@ -228,7 +238,7 @@ describe("RoomWidgetClient", () => {
); );
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org"); expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request"); 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 widgetSendEmitter = new EventEmitter();
const widgetSendPromise = new Promise<void>((resolve) => const widgetSendPromise = new Promise<void>((resolve) =>
widgetSendEmitter.once("send", () => resolve()), widgetSendEmitter.once("send", () => resolve()),
@@ -355,10 +365,10 @@ describe("RoomWidgetClient", () => {
it("handles widget errors with generic error data", async () => { it("handles widget errors with generic error data", async () => {
const error = new Error("failed to send"); 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"] }); await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send); widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
await expect( await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }), client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
@@ -381,22 +391,22 @@ describe("RoomWidgetClient", () => {
response: errorData, 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"] }); await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send); widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
await expect( await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }), client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow(matrixError); ).rejects.toStrictEqual(matrixError);
}); });
}); });
describe("delayed events", () => { describe("delayed events", () => {
describe("when supported", () => { describe("when supported", () => {
const doesServerSupportUnstableFeatureMock = jest.fn((feature) => const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"), Promise.resolve(feature === "org.matrix.msc4140"),
); );
@@ -424,6 +434,7 @@ describe("RoomWidgetClient", () => {
"!1:example.org", "!1:example.org",
2000, 2000,
undefined, undefined,
undefined,
); );
}); });
@@ -444,6 +455,7 @@ describe("RoomWidgetClient", () => {
"!1:example.org", "!1:example.org",
undefined, undefined,
parentDelayId, parentDelayId,
undefined,
); );
}); });
@@ -531,21 +543,63 @@ describe("RoomWidgetClient", () => {
).rejects.toThrow(); ).rejects.toThrow();
}); });
it("updates delayed events", async () => { it.each([UpdateDelayedEventAction.Cancel, UpdateDelayedEventAction.Restart, UpdateDelayedEventAction.Send])(
"can %s scheduled delayed events (action in parameter)",
async (action: UpdateDelayedEventAction) => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(
MatrixCapabilities.MSC4157UpdateDelayedEvent,
);
await client._unstable_updateDelayedEvent("id", action);
let updateDelayedEvent: (delayId: string) => Promise<unknown>;
switch (action) {
case UpdateDelayedEventAction.Cancel:
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
break;
case UpdateDelayedEventAction.Restart:
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
break;
case UpdateDelayedEventAction.Send:
updateDelayedEvent = widgetApi.sendScheduledDelayedEvent;
break;
}
expect(updateDelayedEvent).toHaveBeenCalledWith("id");
},
);
it("can cancel scheduled delayed events (action in method)", async () => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] }); await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent); expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
for (const action of [ await client._unstable_cancelScheduledDelayedEvent("id");
UpdateDelayedEventAction.Cancel, expect(widgetApi.cancelScheduledDelayedEvent).toHaveBeenCalledWith("id");
UpdateDelayedEventAction.Restart, });
UpdateDelayedEventAction.Send,
]) { it("can restart scheduled delayed events (action in method)", async () => {
await client._unstable_updateDelayedEvent("id", action); await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action); expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
} await client._unstable_restartScheduledDelayedEvent("id");
expect(widgetApi.restartScheduledDelayedEvent).toHaveBeenCalledWith("id");
});
it("can send scheduled delayed events (action in method)", async () => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
await client._unstable_sendScheduledDelayedEvent("id");
expect(widgetApi.sendScheduledDelayedEvent).toHaveBeenCalledWith("id");
}); });
}); });
describe("when unsupported", () => { 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 () => { it("fails to send delayed message events", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] }); await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
await expect( await expect(
@@ -583,6 +637,13 @@ describe("RoomWidgetClient", () => {
"Server does not support", "Server does not support",
); );
} }
for (const updateDelayedEvent of [
client._unstable_cancelScheduledDelayedEvent,
client._unstable_restartScheduledDelayedEvent,
client._unstable_sendScheduledDelayedEvent,
]) {
await expect(updateDelayedEvent.call(client, "id")).rejects.toThrow("Server does not support");
}
}); });
}); });
}); });
@@ -729,7 +790,7 @@ describe("RoomWidgetClient", () => {
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve)); const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, 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( widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`, `action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }), new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
@@ -808,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", () => { describe("to-device messages", () => {
const unencryptedContentMap = new Map([ const unencryptedContentMap = new Map([
["@alice:example.org", new Map([["*", { hello: "alice!" }]])], ["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
@@ -929,7 +1146,7 @@ describe("RoomWidgetClient", () => {
it("handles widget errors with generic error data", async () => { it("handles widget errors with generic error data", async () => {
const error = new Error("failed to get token"); const error = new Error("failed to get token");
widgetApi.transport.sendComplete.mockRejectedValue(error); vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(error);
await makeClient({}); await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any); widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
@@ -953,9 +1170,9 @@ describe("RoomWidgetClient", () => {
response: errorData, 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({}); await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any); widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
+3 -3
View File
@@ -37,7 +37,7 @@ describe("eventMapperFor", function () {
setUserCreator(_) {}, setUserCreator(_) {},
} as IStore, } as IStore,
scheduler: { scheduler: {
setProcessFunction: jest.fn(), setProcessFunction: vi.fn(),
} as unknown as MatrixScheduler, } as unknown as MatrixScheduler,
userId: userId, userId: userId,
}); });
@@ -133,7 +133,7 @@ describe("eventMapperFor", function () {
event_id: eventId, event_id: eventId,
}; };
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded"); const decryptEventIfNeededSpy = vi.spyOn(client, "decryptEventIfNeeded");
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
const mapper = eventMapperFor(client, { const mapper = eventMapperFor(client, {
@@ -161,7 +161,7 @@ describe("eventMapperFor", function () {
event_id: eventId, event_id: eventId,
}; };
const evListener = jest.fn(); const evListener = vi.fn();
client.on(MatrixEventEvent.Replaced, evListener); client.on(MatrixEventEvent.Replaced, evListener);
const noReEmitMapper = eventMapperFor(client, { 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. limitations under the License.
*/ */
import { mocked } from "jest-mock"; import { type MockInstance } from "vitest";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { import {
@@ -196,7 +196,7 @@ describe("EventTimelineSet", () => {
it("should aggregate relations which belong to unknown timeline without adding them to any timeline", () => { 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 // 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 reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
const liveTimeline = eventTimelineSet.getLiveTimeline(); const liveTimeline = eventTimelineSet.getLiveTimeline();
@@ -228,7 +228,7 @@ describe("EventTimelineSet", () => {
let thread: Thread; let thread: Thread;
beforeEach(() => { beforeEach(() => {
(client.supportsThreads as jest.Mock).mockReturnValue(true); vi.mocked(client.supportsThreads).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client }); thread = new Thread("!thread_id:server", messageEvent, { room, client });
}); });
@@ -306,20 +306,20 @@ describe("EventTimelineSet", () => {
}); });
describe("with events to be decrypted", () => { describe("with events to be decrypted", () => {
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance; let messageEventShouldAttemptDecryptionSpy: MockInstance;
let messageEventIsDecryptionFailureSpy: jest.SpyInstance; let messageEventIsDecryptionFailureSpy: MockInstance;
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance; let replyEventShouldAttemptDecryptionSpy: MockInstance;
let replyEventIsDecryptionFailureSpy: jest.SpyInstance; let replyEventIsDecryptionFailureSpy: MockInstance;
beforeEach(() => { beforeEach(() => {
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption"); messageEventShouldAttemptDecryptionSpy = vi.spyOn(messageEvent, "shouldAttemptDecryption");
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true); 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); replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure"); replyEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo"); eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
}); });
@@ -384,7 +384,7 @@ describe("EventTimelineSet", () => {
let thread: Thread; let thread: Thread;
beforeEach(() => { beforeEach(() => {
(client.supportsThreads as jest.Mock).mockReturnValue(true); vi.mocked(client.supportsThreads).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client }); 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 * as utils from "../test-utils/test-utils";
import { Direction, EventTimeline } from "../../src/models/event-timeline"; import { Direction, EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state"; import { RoomState } from "../../src/models/room-state";
@@ -20,21 +18,21 @@ describe("EventTimeline", function () {
const getTimeline = (): EventTimeline => { const getTimeline = (): EventTimeline => {
const room = new Room(roomId, mockClient, userA); const room = new Room(roomId, mockClient, userA);
const timelineSet = new EventTimelineSet(room); const timelineSet = new EventTimelineSet(room);
jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet); vi.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
const timeline = new EventTimeline(timelineSet); const timeline = new EventTimeline(timelineSet);
// We manually stub the methods we'll be mocking out later instead of mocking the whole module // 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 // otherwise the default member property values (e.g. paginationToken) will be incorrect
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn(); timeline.getState(Direction.Backward)!.setStateEvents = vi.fn();
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn(); timeline.getState(Direction.Backward)!.getSentinelMember = vi.fn();
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn(); timeline.getState(Direction.Forward)!.setStateEvents = vi.fn();
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn(); timeline.getState(Direction.Forward)!.getSentinelMember = vi.fn();
return timeline; return timeline;
}; };
beforeEach(function () { beforeEach(function () {
// reset any RoomState mocks // reset any RoomState mocks
jest.resetAllMocks(); vi.resetAllMocks();
timeline = getTimeline(); timeline = getTimeline();
}); });
@@ -67,12 +65,12 @@ describe("EventTimeline", function () {
timeline.initialiseState(events); timeline.initialiseState(events);
// @ts-ignore private prop // @ts-ignore private prop
const timelineStartState = timeline.startState!; const timelineStartState = timeline.startState!;
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, { expect(vi.mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
timelineWasEmpty: undefined, timelineWasEmpty: undefined,
}); });
// @ts-ignore private prop // @ts-ignore private prop
const timelineEndState = timeline.endState!; const timelineEndState = timeline.endState!;
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, { expect(vi.mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
timelineWasEmpty: undefined, timelineWasEmpty: undefined,
}); });
}); });
@@ -210,13 +208,13 @@ describe("EventTimeline", function () {
sentinel.name = "Old Alice"; sentinel.name = "Old Alice";
sentinel.membership = KnownMembership.Join; 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) { if (uid === userA) {
return sentinel; return sentinel;
} }
return null; return null;
}); });
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) { vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) { if (uid === userA) {
return oldSentinel; return oldSentinel;
} }
@@ -253,13 +251,13 @@ describe("EventTimeline", function () {
sentinel.name = "Old Alice"; sentinel.name = "Old Alice";
sentinel.membership = KnownMembership.Join; 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) { if (uid === userA) {
return sentinel; return sentinel;
} }
return null; return null;
}); });
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) { vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) { if (uid === userA) {
return oldSentinel; return oldSentinel;
} }
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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", "base": "http://baseUrl",
"params": { "params": {
+1 -1
View File
@@ -57,7 +57,7 @@ describe("MatrixError", () => {
it("should retrieve Date Retry-After header from rate-limit error", () => { it("should retrieve Date Retry-After header from rate-limit error", () => {
headers.set("Retry-After", `${new Date(160000).toUTCString()}`); 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 }); const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true); expect(err.isRateLimitError()).toBe(true);
// prefer Retry-After header over retry_after_ms // prefer Retry-After header over retry_after_ms
+165 -105
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { type Mocked } from "jest-mock"; import { type Mocked, type MockedFunction } from "vitest";
import { FetchHttpApi } from "../../../src/http-api/fetch"; import { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter"; import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
@@ -27,6 +27,7 @@ import {
type IHttpOpts, type IHttpOpts,
MatrixError, MatrixError,
Method, Method,
TokenRefreshError,
} from "../../../src"; } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils"; import { emitPromise } from "../../test-utils/test-utils";
import { type QueryDict, sleep } from "../../../src/utils"; import { type QueryDict, sleep } from "../../../src/utils";
@@ -39,42 +40,42 @@ describe("FetchHttpApi", () => {
const tokenInactiveError = new MatrixError({ errcode: "M_UNKNOWN_TOKEN", error: "Token is not active" }, 401); const tokenInactiveError = new MatrixError({ errcode: "M_UNKNOWN_TOKEN", error: "Token is not active" }, 401);
beforeEach(() => { beforeEach(() => {
jest.useRealTimers(); vi.useRealTimers();
}); });
it("should support aborting multiple times", () => { it("should support aborting multiple times", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
api.request(Method.Get, "/foo"); api.request(Method.Get, "/foo");
api.request(Method.Get, "/baz"); api.request(Method.Get, "/baz");
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy(); 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[0][1]?.signal?.aborted).toBeFalsy();
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy(); expect((fetchFn.mock.calls[1][0] as URL).href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy(); expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeFalsy();
api.abort(); api.abort();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy(); expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy(); expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeTruthy();
api.request(Method.Get, "/bar"); api.request(Method.Get, "/bar");
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy(); expect((fetchFn.mock.calls[2][0] as URL).href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy(); expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeFalsy();
api.abort(); 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", () => { it("should fall back to global fetch if fetchFn not provided", () => {
globalThis.fetch = jest.fn(); const spy = (globalThis.fetch = vi.fn());
expect(globalThis.fetch).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
api.fetch("test"); api.fetch("test");
expect(globalThis.fetch).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
it("should update identity server base url", () => { it("should update identity server base url", () => {
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
expect(api.opts.idBaseUrl).toBeUndefined(); expect(api.opts.idBaseUrl).toBeUndefined();
api.setIdBaseUrl("https://id.foo.bar"); api.setIdBaseUrl("https://id.foo.bar");
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar"); expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
@@ -82,47 +83,68 @@ describe("FetchHttpApi", () => {
describe("idServerRequest", () => { describe("idServerRequest", () => {
it("should throw if no idBaseUrl", () => { it("should throw if no idBaseUrl", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2)).toThrow( expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2)).toThrow(
"No identity server base URL set", "No identity server base URL set",
); );
}); });
it("should send params as query string for GET requests", () => { it("should send params as query string for GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
idBaseUrl,
prefix,
fetchFn,
onlyData: true,
});
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2); 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] as URL).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.getAll("via")).toEqual(["a", "b"]);
}); });
it("should send params as body for non-GET requests", () => { it("should send params as body for non-GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
idBaseUrl,
prefix,
fetchFn,
onlyData: true,
});
const params = { foo: "bar", via: ["a", "b"] }; const params = { foo: "bar", via: ["a", "b"] };
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2); api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar"); expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params); expect(JSON.parse(fetchFn.mock.calls[0][1]!.body as string)).toStrictEqual(params);
}); });
it("should add Authorization header if token provided", () => { it("should add Authorization header if token provided", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
idBaseUrl,
prefix,
fetchFn,
onlyData: true,
});
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token"); 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");
}); });
}); });
it("should return the Response object if onlyData=false", async () => { it("should complain if constructed without `onlyData: true`", async () => {
const res = { ok: true }; expect(
const fetchFn = jest.fn().mockResolvedValue(res); () =>
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false }); new FetchHttpApi(new TypedEventEmitter<any, any>(), {
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res); baseUrl,
prefix,
}),
).toThrow("Constructing FetchHttpApi without `onlyData=true` is no longer supported.");
}); });
it("should set an Accept header, and parse the response as JSON, by default", async () => { it("should set an Accept header, and parse the response as JSON, by default", async () => {
const result = { a: 1 }; 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 }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(result); await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(result);
expect(fetchFn.mock.calls[0][1].headers.Accept).toBe("application/json"); expect(fetchFn.mock.calls[0][1].headers.Accept).toBe("application/json");
@@ -130,7 +152,7 @@ describe("FetchHttpApi", () => {
it("should not set an Accept header, and should return text if json=false", async () => { it("should not set an Accept header, and should return text if json=false", async () => {
const text = "418 I'm a teapot"; 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 }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect( await expect(
api.requestOtherUrl(Method.Get, "http://url", undefined, { api.requestOtherUrl(Method.Get, "http://url", undefined, {
@@ -142,7 +164,7 @@ describe("FetchHttpApi", () => {
it("should not set an Accept header, and should return a blob, if rawResponseBody is true", async () => { it("should not set an Accept header, and should return a blob, if rawResponseBody is true", async () => {
const blob = new Blob(["blobby"]); 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 }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect( await expect(
api.requestOtherUrl(Method.Get, "http://url", undefined, { api.requestOtherUrl(Method.Get, "http://url", undefined, {
@@ -156,7 +178,7 @@ describe("FetchHttpApi", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
prefix, prefix,
fetchFn: jest.fn(), fetchFn: vi.fn(),
onlyData: true, onlyData: true,
}); });
await expect( await expect(
@@ -165,103 +187,109 @@ describe("FetchHttpApi", () => {
}); });
it("should send token via query params if useAuthorizationHeader=false", async () => { it("should send token via query params if useAuthorizationHeader=false", async () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
prefix, prefix,
fetchFn, fetchFn,
accessToken: "token", accessToken: "token",
useAuthorizationHeader: false, useAuthorizationHeader: false,
onlyData: true,
}); });
await api.authedRequest(Method.Get, "/path"); 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 () => { it("should send token via headers by default", async () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
prefix, prefix,
fetchFn, fetchFn,
accessToken: "token", accessToken: "token",
onlyData: true,
}); });
await api.authedRequest(Method.Get, "/path"); 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`", () => { it("should not send a token if not calling `authedRequest`", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
prefix, prefix,
fetchFn, fetchFn,
accessToken: "token", accessToken: "token",
onlyData: true,
}); });
api.request(Method.Get, "/path"); api.request(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy(); expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).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 () => { it("should ensure no token is leaked out via query params if sending via headers", async () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
prefix, prefix,
fetchFn, fetchFn,
accessToken: "token", accessToken: "token",
useAuthorizationHeader: true, useAuthorizationHeader: true,
onlyData: true,
}); });
await api.authedRequest(Method.Get, "/path", { access_token: "123" }); 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][0] as URL).searchParams.get("access_token")).toBeFalsy();
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 override manually specified access token via query params", async () => { it("should not override manually specified access token via query params", async () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
prefix, prefix,
fetchFn, fetchFn,
accessToken: "token", accessToken: "token",
useAuthorizationHeader: false, useAuthorizationHeader: false,
onlyData: true,
}); });
await api.authedRequest(Method.Get, "/path", { access_token: "RealToken" }); 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 () => { it("should not override manually specified access token via header", async () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
prefix, prefix,
fetchFn, fetchFn,
accessToken: "token", accessToken: "token",
useAuthorizationHeader: true, useAuthorizationHeader: true,
onlyData: true,
}); });
await api.authedRequest(Method.Get, "/path", undefined, undefined, { await api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Authorization: "Bearer RealToken" }, 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 () => { it("should not override Accept header", async () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn }); const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await api.authedRequest(Method.Get, "/path", undefined, undefined, { await api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Accept: "text/html" }, 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 () => { it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
const fetchFn = jest.fn().mockResolvedValue({ const fetchFn = vi.fn().mockResolvedValue({
ok: false, ok: false,
headers: { headers: {
get(name: string): string | null { get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null; return name === "Content-Type" ? "application/json" : null;
}, },
}, },
text: jest.fn().mockResolvedValue( text: vi.fn().mockResolvedValue(
JSON.stringify({ JSON.stringify({
errcode: "M_CONSENT_NOT_GIVEN", errcode: "M_CONSENT_NOT_GIVEN",
error: "Ye shall ask for consent", error: "Ye shall ask for consent",
@@ -269,7 +297,7 @@ describe("FetchHttpApi", () => {
), ),
}); });
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn }); const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, onlyData: true });
await Promise.all([ await Promise.all([
emitPromise(emitter, HttpApiEvent.NoConsent), emitPromise(emitter, HttpApiEvent.NoConsent),
@@ -279,11 +307,11 @@ describe("FetchHttpApi", () => {
describe("authedRequest", () => { describe("authedRequest", () => {
it("should not include token if unset", async () => { it("should not include token if unset", async () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true }); const fetchFn = makeMockFetchFn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn }); const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, onlyData: true });
await api.authedRequest(Method.Post, "/account/password"); 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", () => { describe("with refresh token", () => {
@@ -296,7 +324,13 @@ describe("FetchHttpApi", () => {
error: "Token is not active", error: "Token is not active",
soft_logout: false, soft_logout: false,
}; };
const unknownTokenErr = new MatrixError(unknownTokenErrBody, 401); const unknownTokenErr = new MatrixError(
unknownTokenErrBody,
401,
undefined,
undefined,
expect.anything(),
);
const unknownTokenResponse = { const unknownTokenResponse = {
ok: false, ok: false,
status: 401, status: 401,
@@ -305,19 +339,27 @@ describe("FetchHttpApi", () => {
return name === "Content-Type" ? "application/json" : null; return name === "Content-Type" ? "application/json" : null;
}, },
}, },
text: jest.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)), text: vi.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
}; };
const okayResponse = { const okayResponse = {
ok: true, ok: true,
status: 200, status: 200,
json: vi.fn().mockResolvedValue({ x: 1 }),
}; };
describe("without a tokenRefreshFunction", () => { describe("without a tokenRefreshFunction", () => {
it("should emit logout and throw", async () => { 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>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit"); vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, accessToken, refreshToken }); const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
fetchFn,
accessToken,
refreshToken,
onlyData: true,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow( await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
unknownTokenErr, unknownTokenErr,
); );
@@ -328,10 +370,10 @@ describe("FetchHttpApi", () => {
describe("with a tokenRefreshFunction", () => { describe("with a tokenRefreshFunction", () => {
it("should emit logout and throw when token refresh fails", async () => { it("should emit logout and throw when token refresh fails", async () => {
const error = new MatrixError(); const error = new MatrixError();
const tokenRefreshFunction = jest.fn().mockRejectedValue(error); const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse); const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit"); vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, { const api = new FetchHttpApi(emitter, {
baseUrl, baseUrl,
prefix, prefix,
@@ -339,6 +381,7 @@ describe("FetchHttpApi", () => {
tokenRefreshFunction, tokenRefreshFunction,
accessToken, accessToken,
refreshToken, refreshToken,
onlyData: true,
}); });
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow( await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
unknownTokenErr, unknownTokenErr,
@@ -349,10 +392,10 @@ describe("FetchHttpApi", () => {
it("should not emit logout but still throw when token refresh fails due to transitive fault", async () => { it("should not emit logout but still throw when token refresh fails due to transitive fault", async () => {
const error = new ConnectionError("transitive fault"); const error = new ConnectionError("transitive fault");
const tokenRefreshFunction = jest.fn().mockRejectedValue(error); const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse); const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit"); vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, { const api = new FetchHttpApi(emitter, {
baseUrl, baseUrl,
prefix, prefix,
@@ -360,9 +403,10 @@ describe("FetchHttpApi", () => {
tokenRefreshFunction, tokenRefreshFunction,
accessToken, accessToken,
refreshToken, refreshToken,
onlyData: true,
}); });
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow( await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
unknownTokenErr, new TokenRefreshError(unknownTokenErr),
); );
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken); expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr); expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
@@ -371,16 +415,16 @@ describe("FetchHttpApi", () => {
it("should refresh token and retry request", async () => { it("should refresh token and retry request", async () => {
const newAccessToken = "new-access-token"; const newAccessToken = "new-access-token";
const newRefreshToken = "new-refresh-token"; const newRefreshToken = "new-refresh-token";
const tokenRefreshFunction = jest.fn().mockResolvedValue({ const tokenRefreshFunction = vi.fn().mockResolvedValue({
accessToken: newAccessToken, accessToken: newAccessToken,
refreshToken: newRefreshToken, refreshToken: newRefreshToken,
}); });
const fetchFn = jest const fetchFn = vi
.fn() .fn()
.mockResolvedValueOnce(unknownTokenResponse) .mockResolvedValueOnce(unknownTokenResponse)
.mockResolvedValueOnce(okayResponse); .mockResolvedValueOnce(okayResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit"); vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, { const api = new FetchHttpApi(emitter, {
baseUrl, baseUrl,
prefix, prefix,
@@ -388,11 +432,12 @@ describe("FetchHttpApi", () => {
tokenRefreshFunction, tokenRefreshFunction,
accessToken, accessToken,
refreshToken, refreshToken,
onlyData: true,
}); });
const result = await api.authedRequest(Method.Post, "/account/password", undefined, undefined, { const result = await api.authedRequest(Method.Post, "/account/password", undefined, undefined, {
headers: {}, headers: {},
}); });
expect(result).toEqual(okayResponse); expect(result).toEqual({ x: 1 });
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken); expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
expect(fetchFn).toHaveBeenCalledTimes(2); expect(fetchFn).toHaveBeenCalledTimes(2);
@@ -411,7 +456,7 @@ describe("FetchHttpApi", () => {
// count because it's only to get a token with an expiry) // count because it's only to get a token with an expiry)
const newAccessToken = "new-access-token"; const newAccessToken = "new-access-token";
const newRefreshToken = "new-refresh-token"; const newRefreshToken = "new-refresh-token";
const tokenRefreshFunction = jest.fn().mockReturnValue({ const tokenRefreshFunction = vi.fn().mockReturnValue({
accessToken: newAccessToken, accessToken: newAccessToken,
refreshToken: newRefreshToken, refreshToken: newRefreshToken,
// This needs to be sufficiently high that it's over the threshold for // This needs to be sufficiently high that it's over the threshold for
@@ -420,10 +465,10 @@ describe("FetchHttpApi", () => {
}); });
// fetch doesn't like our new or old tokens // 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>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit"); vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, { const api = new FetchHttpApi(emitter, {
baseUrl, baseUrl,
prefix, prefix,
@@ -431,8 +476,9 @@ describe("FetchHttpApi", () => {
tokenRefreshFunction, tokenRefreshFunction,
accessToken, accessToken,
refreshToken, refreshToken,
onlyData: true,
}); });
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow( await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
unknownTokenErr, unknownTokenErr,
); );
@@ -454,7 +500,7 @@ describe("FetchHttpApi", () => {
// first refresh is to get a token with an expiry at all, because we // first refresh is to get a token with an expiry at all, because we
// can't specify an expiry on the token we inject // can't specify an expiry on the token we inject
const tokenRefreshFunction = jest.fn().mockResolvedValueOnce({ const tokenRefreshFunction = vi.fn().mockResolvedValueOnce({
accessToken: newAccessToken, accessToken: newAccessToken,
refreshToken: newRefreshToken, refreshToken: newRefreshToken,
expiry: new Date(Date.now() + 1000), expiry: new Date(Date.now() + 1000),
@@ -475,10 +521,10 @@ describe("FetchHttpApi", () => {
expiry: new Date(Date.now() + 5 * 60 * 1000), 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>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit"); vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, { const api = new FetchHttpApi(emitter, {
baseUrl, baseUrl,
prefix, prefix,
@@ -486,8 +532,9 @@ describe("FetchHttpApi", () => {
tokenRefreshFunction, tokenRefreshFunction,
accessToken, accessToken,
refreshToken, refreshToken,
onlyData: true,
}); });
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow( await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
unknownTokenErr, unknownTokenErr,
); );
@@ -504,9 +551,9 @@ describe("FetchHttpApi", () => {
const localBaseUrl = "http://baseurl"; const localBaseUrl = "http://baseurl";
const baseUrlWithTrailingSlash = "http://baseurl/"; const baseUrlWithTrailingSlash = "http://baseurl/";
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => { const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
const fetchFn = jest.fn(); const fetchFn = vi.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn }); return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn, onlyData: true });
}; };
type TestParams = { type TestParams = {
@@ -557,9 +604,15 @@ describe("FetchHttpApi", () => {
describe("extraParams handling", () => { describe("extraParams handling", () => {
const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => { const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => {
const fetchFn = jest.fn(); const fetchFn = vi.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
return new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, extraParams }); return new FetchHttpApi(emitter, {
baseUrl: localBaseUrl,
prefix,
fetchFn,
onlyData: true,
extraParams,
});
}; };
const userId = "@rsb-tbg:localhost"; const userId = "@rsb-tbg:localhost";
@@ -610,9 +663,9 @@ describe("FetchHttpApi", () => {
}); });
it("should work when extraParams is undefined", () => { it("should work when extraParams is undefined", () => {
const fetchFn = jest.fn(); const fetchFn = vi.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(); const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn }); const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, onlyData: true });
const queryParams = { userId: "123" }; const queryParams = { userId: "123" };
const result = api.getUrl("/test", queryParams); const result = api.getUrl("/test", queryParams);
@@ -634,21 +687,22 @@ describe("FetchHttpApi", () => {
}); });
it("should not log query parameters", async () => { it("should not log query parameters", async () => {
jest.useFakeTimers(); vi.useFakeTimers();
const responseResolvers = Promise.withResolvers<Response>(); const responseResolvers = Promise.withResolvers<Response>();
const fetchFn = jest.fn().mockReturnValue(responseResolvers.promise); const fetchFn = vi.fn().mockReturnValue(responseResolvers.promise);
const mockLogger = { const mockLogger = {
debug: jest.fn(), debug: vi.fn(),
} as unknown as Mocked<Logger>; } as unknown as Mocked<Logger>;
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
prefix, prefix,
fetchFn, fetchFn,
logger: mockLogger, logger: mockLogger,
onlyData: true,
}); });
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment"); 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, text: () => Promise.resolve("RESPONSE") } as Response); responseResolvers.resolve({ ok: true, status: 200, json: () => Promise.resolve("RESPONSE") } as Response);
await prom; await prom;
expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment"); expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
expect(mockLogger.debug).not.toHaveBeenCalledWith("query"); expect(mockLogger.debug).not.toHaveBeenCalledWith("query");
@@ -668,7 +722,7 @@ describe("FetchHttpApi", () => {
it("should not make multiple concurrent refresh token requests", async () => { it("should not make multiple concurrent refresh token requests", async () => {
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>(); const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
const fetchFn = jest.fn().mockResolvedValue({ const fetchFn = vi.fn().mockResolvedValue({
ok: false, ok: false,
status: tokenInactiveError.httpStatus, status: tokenInactiveError.httpStatus,
async text() { async text() {
@@ -678,10 +732,10 @@ describe("FetchHttpApi", () => {
return tokenInactiveError.data; return tokenInactiveError.data;
}, },
headers: { 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>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
@@ -691,6 +745,7 @@ describe("FetchHttpApi", () => {
tokenRefreshFunction, tokenRefreshFunction,
accessToken: "ACCESS_TOKEN", accessToken: "ACCESS_TOKEN",
refreshToken: "REFRESH_TOKEN", refreshToken: "REFRESH_TOKEN",
onlyData: true,
}); });
const prom1 = api.authedRequest(Method.Get, "/path1"); const prom1 = api.authedRequest(Method.Get, "/path1");
@@ -708,7 +763,7 @@ describe("FetchHttpApi", () => {
return {}; return {};
}, },
headers: { headers: {
get: jest.fn().mockReturnValue("application/json"), get: vi.fn().mockReturnValue("application/json"),
}, },
}); });
deferredTokenRefresh.resolve({ accessToken: "NEW_ACCESS_TOKEN", refreshToken: "NEW_REFRESH_TOKEN" }); deferredTokenRefresh.resolve({ accessToken: "NEW_ACCESS_TOKEN", refreshToken: "NEW_REFRESH_TOKEN" });
@@ -723,7 +778,7 @@ describe("FetchHttpApi", () => {
it("should use newly refreshed token if request starts mid-refresh", async () => { it("should use newly refreshed token if request starts mid-refresh", async () => {
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>(); const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
const fetchFn = jest.fn().mockResolvedValue({ const fetchFn = vi.fn().mockResolvedValue({
ok: false, ok: false,
status: tokenInactiveError.httpStatus, status: tokenInactiveError.httpStatus,
async text() { async text() {
@@ -733,10 +788,10 @@ describe("FetchHttpApi", () => {
return tokenInactiveError.data; return tokenInactiveError.data;
}, },
headers: { 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>(), { const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl, baseUrl,
@@ -746,6 +801,7 @@ describe("FetchHttpApi", () => {
tokenRefreshFunction, tokenRefreshFunction,
accessToken: "ACCESS_TOKEN", accessToken: "ACCESS_TOKEN",
refreshToken: "REFRESH_TOKEN", refreshToken: "REFRESH_TOKEN",
onlyData: true,
}); });
const prom1 = api.authedRequest(Method.Get, "/path1"); const prom1 = api.authedRequest(Method.Get, "/path1");
@@ -765,7 +821,7 @@ describe("FetchHttpApi", () => {
return {}; return {};
}, },
headers: { headers: {
get: jest.fn().mockReturnValue("application/json"), get: vi.fn().mockReturnValue("application/json"),
}, },
}); });
@@ -783,3 +839,7 @@ describe("FetchHttpApi", () => {
expect(api.opts.refreshToken).toBe("NEW_REFRESH_TOKEN"); expect(api.opts.refreshToken).toBe("NEW_REFRESH_TOKEN");
}); });
}); });
function makeMockFetchFn(): MockedFunction<Window["fetch"]> {
return vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
}
+93 -75
View File
@@ -14,66 +14,71 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { mocked } from "jest-mock";
import { ClientPrefix, MatrixHttpApi, Method, type UploadResponse } from "../../../src"; import { ClientPrefix, MatrixHttpApi, Method, type UploadResponse } from "../../../src";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter"; import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
type Writeable<T> = { -readonly [P in keyof T]: T[P] }; type Writeable<T> = { -readonly [P in keyof T]: T[P] };
jest.useFakeTimers(); vi.useFakeTimers();
describe("MatrixHttpApi", () => { describe("MatrixHttpApi", () => {
const baseUrl = "http://baseUrl"; const baseUrl = "http://baseUrl";
const prefix = ClientPrefix.V3; const prefix = ClientPrefix.V3;
let xhr: Writeable<XMLHttpRequest>;
let upload: Promise<UploadResponse>; let upload: Promise<UploadResponse>;
const DONE = 0; const DONE = 0;
function getRequest(): Writeable<XMLHttpRequest> | undefined {
return vi.mocked(globalThis.XMLHttpRequest)?.mock.instances.at(-1);
}
beforeEach(() => { beforeEach(() => {
xhr = { // We stub out XHR here as it is not available in the test environment
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
// @ts-ignore // @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 // @ts-ignore
globalThis.XMLHttpRequest.DONE = DONE; globalThis.XMLHttpRequest.DONE = DONE;
}); });
afterEach(() => { afterEach(() => {
upload?.catch(() => {}); upload?.catch(() => {});
// Abort any remaining requests const xhr = getRequest();
xhr.readyState = DONE; if (xhr) {
xhr.status = 0; // Abort any remaining requests
// @ts-ignore xhr.readyState = DONE;
xhr.onreadystatechange?.(new Event("test")); xhr.status = 0;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
}
}); });
it("should fall back to `fetch` where xhr is unavailable", async () => { it("should fall back to `fetch` where xhr is unavailable", async () => {
globalThis.XMLHttpRequest = undefined!; 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 }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
await upload; await upload;
expect(fetchFn).toHaveBeenCalled(); expect(fetchFn).toHaveBeenCalled();
}); });
it("should prefer xhr where available", () => { 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 }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
expect(fetchFn).not.toHaveBeenCalled(); expect(fetchFn).not.toHaveBeenCalled();
expect(xhr.open).toHaveBeenCalled(); expect(getRequest()!.open).toHaveBeenCalled();
}); });
it("should send access token in query params if header disabled", () => { it("should send access token in query params if header disabled", () => {
@@ -82,13 +87,14 @@ describe("MatrixHttpApi", () => {
prefix, prefix,
accessToken: "token", accessToken: "token",
useAuthorizationHeader: false, useAuthorizationHeader: false,
onlyData: true,
}); });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith( expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post, Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token", 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", () => { it("should send access token in header by default", () => {
@@ -96,131 +102,138 @@ describe("MatrixHttpApi", () => {
baseUrl, baseUrl,
prefix, prefix,
accessToken: "token", accessToken: "token",
onlyData: true,
}); });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload"); expect(getRequest()!.open).toHaveBeenCalledWith(
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token"); Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
);
expect(getRequest()!.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
}); });
it("should include filename by default", () => { it("should include filename by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File, { name: "name" }); upload = api.uploadContent({} as File, { name: "name" });
expect(xhr.open).toHaveBeenCalledWith( expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post, Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name", baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
); );
}); });
it("should allow not sending the filename", () => { it("should allow not sending the filename", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File, { name: "name", includeFilename: false }); 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", () => { it("should abort xhr when the upload is aborted", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
api.cancelUpload(upload); api.cancelUpload(upload);
expect(xhr.abort).toHaveBeenCalled(); expect(getRequest()!.abort).toHaveBeenCalled();
return expect(upload).rejects.toThrow("Aborted"); return expect(upload).rejects.toThrow("Aborted");
}); });
it("should timeout if no progress in 30s", () => { it("should timeout if no progress in 30s", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
jest.advanceTimersByTime(25000); vi.advanceTimersByTime(25000);
// @ts-ignore // @ts-ignore
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 })); getRequest()!.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
jest.advanceTimersByTime(25000); vi.advanceTimersByTime(25000);
expect(xhr.abort).not.toHaveBeenCalled(); expect(getRequest()!.abort).not.toHaveBeenCalled();
jest.advanceTimersByTime(5000); vi.advanceTimersByTime(5000);
expect(xhr.abort).toHaveBeenCalled(); expect(getRequest()!.abort).toHaveBeenCalled();
}); });
it("should call progressHandler", () => { it("should call progressHandler", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); 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 }); upload = api.uploadContent({} as File, { progressHandler });
const progressEvent = new Event("progress") as ProgressEvent; const progressEvent = new Event("progress") as ProgressEvent;
Object.assign(progressEvent, { loaded: 1, total: 100 }); Object.assign(progressEvent, { loaded: 1, total: 100 });
// @ts-ignore // @ts-ignore
xhr.upload.onprogress(progressEvent); getRequest()!.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 }); expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
Object.assign(progressEvent, { loaded: 95, total: 100 }); Object.assign(progressEvent, { loaded: 95, total: 100 });
// @ts-ignore // @ts-ignore
xhr.upload.onprogress(progressEvent); getRequest()!.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 }); expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
}); });
it("should error when no response body", () => { it("should error when no response body", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
xhr.readyState = DONE; getRequest()!.readyState = DONE;
xhr.responseText = ""; getRequest()!.responseText = "";
xhr.status = 200; getRequest()!.status = 200;
// @ts-ignore // @ts-ignore
xhr.onreadystatechange?.(new Event("test")); getRequest()!.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("No response body."); return expect(upload).rejects.toThrow("No response body.");
}); });
it("should error on a 400-code", () => { it("should error on a 400-code", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
xhr.readyState = DONE; getRequest()!.readyState = DONE;
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}'; getRequest()!.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
xhr.status = 404; getRequest()!.status = 404;
mocked(xhr.getResponseHeader).mockImplementation((name) => vi.mocked(getRequest()!.getResponseHeader).mockImplementation((name) =>
name.toLowerCase() === "content-type" ? "application/json" : null, 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 // @ts-ignore
xhr.onreadystatechange?.(new Event("test")); getRequest()!.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("Not found"); return expect(upload).rejects.toThrow("Not found");
}); });
it("should return response on successful upload", () => { it("should return response on successful upload", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
xhr.readyState = DONE; getRequest()!.readyState = DONE;
xhr.responseText = '{"content_uri": "mxc://server/foobar"}'; getRequest()!.responseText = '{"content_uri": "mxc://server/foobar"}';
xhr.status = 200; getRequest()!.status = 200;
mocked(xhr.getResponseHeader).mockReturnValue("application/json"); vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore // @ts-ignore
xhr.onreadystatechange?.(new Event("test")); getRequest()!.onreadystatechange?.(new Event("test"));
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" }); return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
}); });
it("should abort xhr when calling `cancelUpload`", () => { it("should abort xhr when calling `cancelUpload`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
expect(api.cancelUpload(upload)).toBeTruthy(); expect(api.cancelUpload(upload)).toBeTruthy();
expect(xhr.abort).toHaveBeenCalled(); expect(getRequest()!.abort).toHaveBeenCalled();
}); });
it("should return false when `cancelUpload` is called but unsuccessful", async () => { it("should return false when `cancelUpload` is called but unsuccessful", async () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
xhr.readyState = DONE; getRequest()!.readyState = DONE;
xhr.status = 500; getRequest()!.status = 500;
mocked(xhr.getResponseHeader).mockReturnValue("application/json"); vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore // @ts-ignore
xhr.onreadystatechange?.(new Event("test")); getRequest()!.onreadystatechange?.(new Event("test"));
await upload.catch(() => {}); await upload.catch(() => {});
expect(api.cancelUpload(upload)).toBeFalsy(); expect(api.cancelUpload(upload)).toBeFalsy();
expect(xhr.abort).not.toHaveBeenCalled(); expect(getRequest()!.abort).not.toHaveBeenCalled();
}); });
it("should return active uploads in `getCurrentUploads`", () => { it("should return active uploads in `getCurrentUploads`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File); upload = api.uploadContent({} as File);
expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeTruthy(); expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeTruthy();
api.cancelUpload(upload); api.cancelUpload(upload);
@@ -228,7 +241,12 @@ describe("MatrixHttpApi", () => {
}); });
it("should return expected object from `getContentUri`", () => { it("should return expected object from `getContentUri`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, accessToken: "token" }); const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
accessToken: "token",
onlyData: true,
});
expect(api.getContentUri()).toMatchSnapshot(); expect(api.getContentUri()).toMatchSnapshot();
}); });
}); });
+72 -27
View File
@@ -14,51 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { mocked } from "jest-mock";
import { import {
anySignal, anySignal,
ConnectionError, ConnectionError,
HTTPError, HTTPError,
MatrixError, MatrixError,
MatrixSafetyError,
MatrixSafetyErrorCode,
parseErrorResponse, parseErrorResponse,
retryNetworkOperation, retryNetworkOperation,
timeoutSignal, timeoutSignal,
} from "../../../src"; } from "../../../src";
import { sleep } from "../../../src/utils"; import { sleep } from "../../../src/utils";
jest.mock("../../../src/utils"); vi.mock("../../../src/utils");
// setupTests mocks `timeoutSignal` due to hanging timers // setupTests mocks `timeoutSignal` due to hanging timers
jest.unmock("../../../src/http-api/utils"); vi.unmock("../../../src/http-api/utils");
describe("timeoutSignal", () => { describe("timeoutSignal", () => {
jest.useFakeTimers(); vi.useFakeTimers();
it("should fire abort signal after specified timeout", () => { it("should fire abort signal after specified timeout", () => {
const signal = timeoutSignal(3000); const signal = timeoutSignal(3000);
const onabort = jest.fn(); const onabort = vi.fn();
signal.onabort = onabort; signal.onabort = onabort;
expect(signal.aborted).toBeFalsy(); expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled(); expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(3000); vi.advanceTimersByTime(3000);
expect(signal.aborted).toBeTruthy(); expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled(); expect(onabort).toHaveBeenCalled();
}); });
}); });
describe("anySignal", () => { describe("anySignal", () => {
jest.useFakeTimers(); vi.useFakeTimers();
it("should fire when any signal fires", () => { it("should fire when any signal fires", () => {
const { signal } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]); const { signal } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
const onabort = jest.fn(); const onabort = vi.fn();
signal.onabort = onabort; signal.onabort = onabort;
expect(signal.aborted).toBeFalsy(); expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled(); expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(2000); vi.advanceTimersByTime(2000);
expect(signal.aborted).toBeTruthy(); expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled(); expect(onabort).toHaveBeenCalled();
}); });
@@ -66,13 +66,13 @@ describe("anySignal", () => {
it("should cleanup when instructed", () => { it("should cleanup when instructed", () => {
const { signal, cleanup } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]); const { signal, cleanup } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
const onabort = jest.fn(); const onabort = vi.fn();
signal.onabort = onabort; signal.onabort = onabort;
expect(signal.aborted).toBeFalsy(); expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled(); expect(onabort).not.toHaveBeenCalled();
cleanup(); cleanup();
jest.advanceTimersByTime(2000); vi.advanceTimersByTime(2000);
expect(signal.aborted).toBeFalsy(); expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled(); expect(onabort).not.toHaveBeenCalled();
}); });
@@ -86,9 +86,14 @@ describe("anySignal", () => {
}); });
describe("parseErrorResponse", () => { describe("parseErrorResponse", () => {
const url = "https://example.org";
let headers: Headers; let headers: Headers;
const xhrHeaderMethods = { const xhrHeaderMethods = {
getResponseHeader: (name: string) => headers.get(name), responseURL: url,
getResponseHeader: (name: string) => {
headers.get(name);
},
getAllResponseHeaders: () => { getAllResponseHeaders: () => {
let allHeaders = ""; let allHeaders = "";
headers.forEach((value, key) => { headers.forEach((value, key) => {
@@ -118,6 +123,9 @@ describe("parseErrorResponse", () => {
errcode: "TEST", errcode: "TEST",
}, },
500, 500,
url,
undefined,
expect.any(Headers),
), ),
); );
}); });
@@ -127,6 +135,7 @@ describe("parseErrorResponse", () => {
expect( expect(
parseErrorResponse( parseErrorResponse(
{ {
url,
headers, headers,
status: 500, status: 500,
} as Response, } as Response,
@@ -138,6 +147,9 @@ describe("parseErrorResponse", () => {
errcode: "TEST", errcode: "TEST",
}, },
500, 500,
url,
undefined,
expect.any(Headers),
), ),
); );
}); });
@@ -147,8 +159,8 @@ describe("parseErrorResponse", () => {
expect( expect(
parseErrorResponse( parseErrorResponse(
{ {
responseURL: "https://example.com",
...xhrHeaderMethods, ...xhrHeaderMethods,
responseURL: "https://example.com",
status: 500, status: 500,
} as XMLHttpRequest, } as XMLHttpRequest,
'{"errcode": "TEST"}', '{"errcode": "TEST"}',
@@ -160,6 +172,8 @@ describe("parseErrorResponse", () => {
}, },
500, 500,
"https://example.com", "https://example.com",
undefined,
expect.any(Headers),
), ),
); );
}); });
@@ -182,9 +196,40 @@ describe("parseErrorResponse", () => {
}, },
500, 500,
"https://example.com", "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", () => { describe("with HTTP headers", () => {
function addHeaders(headers: Headers) { function addHeaders(headers: Headers) {
@@ -196,7 +241,7 @@ describe("parseErrorResponse", () => {
} }
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) { 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", () => { it("should resolve HTTP Errors from XHR with headers", () => {
@@ -265,7 +310,7 @@ describe("parseErrorResponse", () => {
} as Response, } as Response,
'{"errcode": "TEST"}', '{"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", () => { it("should handle empty type gracefully", () => {
@@ -304,27 +349,27 @@ describe("parseErrorResponse", () => {
} as Response, } as Response,
"I'm a teapot", "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", () => { describe("retryNetworkOperation", () => {
it("should retry given number of times with exponential sleeps", async () => { it("should retry given number of times with exponential sleeps", async () => {
const err = new ConnectionError("test"); const err = new ConnectionError("test");
const fn = jest.fn().mockRejectedValue(err); const fn = vi.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined); vi.mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err); await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(4); expect(fn).toHaveBeenCalledTimes(4);
expect(mocked(sleep)).toHaveBeenCalledTimes(3); expect(vi.mocked(sleep)).toHaveBeenCalledTimes(3);
expect(mocked(sleep).mock.calls[0][0]).toBe(2000); expect(vi.mocked(sleep).mock.calls[0][0]).toBe(2000);
expect(mocked(sleep).mock.calls[1][0]).toBe(4000); expect(vi.mocked(sleep).mock.calls[1][0]).toBe(4000);
expect(mocked(sleep).mock.calls[2][0]).toBe(8000); expect(vi.mocked(sleep).mock.calls[2][0]).toBe(8000);
}); });
it("should bail out on errors other than ConnectionError", async () => { it("should bail out on errors other than ConnectionError", async () => {
const err = new TypeError("invalid JSON"); const err = new TypeError("invalid JSON");
const fn = jest.fn().mockRejectedValue(err); const fn = vi.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined); vi.mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err); await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledTimes(1);
}); });
@@ -334,10 +379,10 @@ describe("retryNetworkOperation", () => {
const err2 = new ConnectionError("test2"); const err2 = new ConnectionError("test2");
const err3 = new ConnectionError("test3"); const err3 = new ConnectionError("test3");
const errors = [err1, err2, err3]; const errors = [err1, err2, err3];
const fn = jest.fn().mockImplementation(() => { const fn = vi.fn().mockImplementation(() => {
throw errors.shift(); throw errors.shift();
}); });
mocked(sleep).mockResolvedValue(undefined); vi.mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3); 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 { type MatrixClient } from "../../src/client";
import { logger } from "../../src/logger"; 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 { HTTPError, MatrixError } from "../../src/http-api";
import { sleep } from "../../src/utils"; import { sleep } from "../../src/utils";
import { secureRandomString } from "../../src/randomstring"; import { secureRandomString } from "../../src/randomstring";
@@ -34,14 +34,14 @@ const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixC
describe("InteractiveAuth", () => { describe("InteractiveAuth", () => {
it("should start an auth stage and complete it", async () => { it("should start an auth stage and complete it", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest: doRequest, doRequest: doRequest,
stateUpdated: stateUpdated, stateUpdated: stateUpdated,
requestEmailToken: jest.fn(), requestEmailToken: vi.fn(),
authData: { authData: {
session: "sessionId", session: "sessionId",
flows: [{ stages: [AuthType.Password] }], flows: [{ stages: [AuthType.Password] }],
@@ -83,14 +83,14 @@ describe("InteractiveAuth", () => {
}); });
it("should handle auth errcode presence", async () => { it("should handle auth errcode presence", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest: doRequest, doRequest: doRequest,
stateUpdated: stateUpdated, stateUpdated: stateUpdated,
requestEmailToken: jest.fn(), requestEmailToken: vi.fn(),
authData: { authData: {
session: "sessionId", session: "sessionId",
flows: [{ stages: [AuthType.Password] }], flows: [{ stages: [AuthType.Password] }],
@@ -132,9 +132,9 @@ describe("InteractiveAuth", () => {
}); });
it("should handle set emailSid for email flow", async () => { it("should handle set emailSid for email flow", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
doRequest, doRequest,
@@ -186,9 +186,9 @@ describe("InteractiveAuth", () => {
}); });
it("should make a request if no authdata is provided", async () => { it("should make a request if no authdata is provided", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
@@ -248,9 +248,9 @@ describe("InteractiveAuth", () => {
}); });
it("should make a request if authdata is null", async () => { it("should make a request if authdata is null", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
@@ -310,9 +310,9 @@ describe("InteractiveAuth", () => {
}); });
it("should start an auth stage and reject if no auth flow", async () => { it("should start an auth stage and reject if no auth flow", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
@@ -337,13 +337,15 @@ describe("InteractiveAuth", () => {
throw err; 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 () => { it("should start an auth stage and reject if no auth flow but has session", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
@@ -372,13 +374,15 @@ describe("InteractiveAuth", () => {
throw err; 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 () => { it("should handle unexpected error types without data property set", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
@@ -397,13 +401,13 @@ describe("InteractiveAuth", () => {
throw err; 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 () => { it("should allow dummy auth", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
@@ -435,9 +439,9 @@ describe("InteractiveAuth", () => {
describe("requestEmailToken", () => { describe("requestEmailToken", () => {
it("increases auth attempts", async () => { it("increases auth attempts", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
requestEmailToken.mockImplementation(async () => ({ sid: "" })); requestEmailToken.mockImplementation(async () => ({ sid: "" }));
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
@@ -464,9 +468,9 @@ describe("InteractiveAuth", () => {
}); });
it("passes errors through", async () => { it("passes errors through", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
requestEmailToken.mockImplementation(async () => { requestEmailToken.mockImplementation(async () => {
throw new Error("unspecific network error"); throw new Error("unspecific network error");
}); });
@@ -482,9 +486,9 @@ describe("InteractiveAuth", () => {
}); });
it("only starts one request at a time", async () => { it("only starts one request at a time", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" })); requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
@@ -499,9 +503,9 @@ describe("InteractiveAuth", () => {
}); });
it("stores result in email sid", async () => { it("stores result in email sid", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const requestEmailToken = jest.fn(); const requestEmailToken = vi.fn();
const sid = secureRandomString(24); const sid = secureRandomString(24);
requestEmailToken.mockImplementation(() => sleep(500, { sid })); requestEmailToken.mockImplementation(() => sleep(500, { sid }));
@@ -518,14 +522,14 @@ describe("InteractiveAuth", () => {
}); });
it("should prioritise shorter flows", async () => { it("should prioritise shorter flows", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest: doRequest, doRequest: doRequest,
stateUpdated: stateUpdated, stateUpdated: stateUpdated,
requestEmailToken: jest.fn(), requestEmailToken: vi.fn(),
authData: { authData: {
session: "sessionId", session: "sessionId",
flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }], flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
@@ -539,14 +543,14 @@ describe("InteractiveAuth", () => {
}); });
it("should prioritise flows with entirely supported stages", async () => { it("should prioritise flows with entirely supported stages", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest: doRequest, doRequest: doRequest,
stateUpdated: stateUpdated, stateUpdated: stateUpdated,
requestEmailToken: jest.fn(), requestEmailToken: vi.fn(),
authData: { authData: {
session: "sessionId", session: "sessionId",
flows: [{ stages: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }], 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 () => { it("should fire stateUpdated callback with error when a request fails", async () => {
const doRequest = jest.fn(); const doRequest = vi.fn();
const stateUpdated = jest.fn(); const stateUpdated = vi.fn();
const ia = new InteractiveAuth({ const ia = new InteractiveAuth({
matrixClient: getFakeClient(), matrixClient: getFakeClient(),
doRequest: doRequest, doRequest: doRequest,
stateUpdated: stateUpdated, stateUpdated: stateUpdated,
requestEmailToken: jest.fn(), requestEmailToken: vi.fn(),
authData: { authData: {
session: "sessionId", session: "sessionId",
flows: [{ stages: [AuthType.Password] }], flows: [{ stages: [AuthType.Password] }],
+1 -1
View File
@@ -23,7 +23,7 @@ let client: MatrixClient;
describe("Local notification settings", () => { describe("Local notification settings", () => {
beforeEach(() => { beforeEach(() => {
client = new TestClient("@alice:matrix.org", "123", undefined, undefined, undefined).client; 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", () => { 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"; import { DebugLogger, logger } from "../../src/logger.ts";
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); vi.restoreAllMocks();
}); });
describe("logger", () => { describe("logger", () => {
it("should log to console by default", () => { it("should log to console by default", () => {
jest.spyOn(console, "debug").mockReturnValue(undefined); vi.spyOn(console, "debug").mockReturnValue(undefined);
logger.debug("test1"); logger.debug("test1");
logger.log("test2"); logger.log("test2");
@@ -35,8 +35,8 @@ describe("logger", () => {
}); });
it("should allow creation of child loggers which add a prefix", () => { it("should allow creation of child loggers which add a prefix", () => {
jest.spyOn(loglevel, "getLogger"); vi.spyOn(loglevel, "getLogger");
jest.spyOn(console, "debug").mockReturnValue(undefined); vi.spyOn(console, "debug").mockReturnValue(undefined);
const childLogger = logger.getChild("[prefix1]"); const childLogger = logger.getChild("[prefix1]");
expect(loglevel.getLogger).toHaveBeenCalledWith("matrix-[prefix1]"); expect(loglevel.getLogger).toHaveBeenCalledWith("matrix-[prefix1]");
@@ -52,7 +52,7 @@ describe("logger", () => {
describe("DebugLogger", () => { describe("DebugLogger", () => {
it("should handle empty log messages", () => { it("should handle empty log messages", () => {
const mockTarget = jest.fn(); const mockTarget = vi.fn();
const logger = new DebugLogger(mockTarget as any); const logger = new DebugLogger(mockTarget as any);
logger.info(); logger.info();
expect(mockTarget).toHaveBeenCalledTimes(1); expect(mockTarget).toHaveBeenCalledTimes(1);
@@ -60,7 +60,7 @@ describe("DebugLogger", () => {
}); });
it("should handle logging an Error", () => { it("should handle logging an Error", () => {
const mockTarget = jest.fn(); const mockTarget = vi.fn();
const logger = new DebugLogger(mockTarget as any); const logger = new DebugLogger(mockTarget as any);
// If there is a stack and a message, we use the stack. // If there is a stack and a message, we use the stack.
@@ -79,7 +79,7 @@ describe("DebugLogger", () => {
}); });
it("should handle logging an object", () => { it("should handle logging an object", () => {
const mockTarget = jest.fn(); const mockTarget = vi.fn();
const logger = new DebugLogger(mockTarget as any); const logger = new DebugLogger(mockTarget as any);
const obj = { a: 1 }; 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 { ClientPrefix, MatrixClient } from "../../src";
import { SSOAction } from "../../src/@types/auth"; 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 urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
const url = new URL(urlString); const url = new URL(urlString);
expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false); expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
expect(url.searchParams.has("action")).toBe(false);
}); });
it("register", function () { it("register", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER); const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
const url = new URL(urlString); const url = new URL(urlString);
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register"); expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
expect(url.searchParams.get("action")).toEqual("register");
}); });
it("login", function () { it("login", function () {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN); const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
const url = new URL(urlString); const url = new URL(urlString);
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login"); expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
expect(url.searchParams.get("action")).toEqual("login");
}); });
}); });
}); });
describe("refreshToken", () => { describe("refreshToken", () => {
afterEach(() => {
fetchMock.mockReset();
});
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => { it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
const client = createExampleMatrixClient(); const client = createExampleMatrixClient();
File diff suppressed because it is too large Load Diff
+356 -50
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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -14,65 +14,71 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { type MatrixEvent } from "../../../src"; import { type RtcMembershipData, type SessionMembershipData } from "../../../src/matrixrtc/membershipData/index.ts";
import { import { type IContent, type MatrixEvent } from "../../../src/models/event.ts";
CallMembership, import { EventType } from "../../../src/@types/event.ts";
type SessionMembershipData, import { CallMembership, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership.ts";
DEFAULT_EXPIRE_DURATION,
} from "../../../src/matrixrtc/CallMembership";
import { membershipTemplate } from "./mocks";
function makeMockEvent(originTs = 0): MatrixEvent { function createCallMembership(ev: MatrixEvent, content: IContent): CallMembership {
return { vi.mocked(ev.getContent).mockReturnValue(content);
getTs: jest.fn().mockReturnValue(originTs), const data = CallMembership.membershipDataFromMatrixEvent(ev);
getSender: jest.fn().mockReturnValue("@alice:example.org"), return new CallMembership(ev, data, "xx");
} as unknown as MatrixEvent;
} }
describe("CallMembership", () => { describe("CallMembership", () => {
describe("SessionMembershipData", () => { 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(() => { beforeEach(() => {
jest.useFakeTimers(); vi.useFakeTimers();
}); });
afterEach(() => { afterEach(() => {
jest.useRealTimers(); vi.useRealTimers();
}); });
const membershipTemplate: SessionMembershipData = { const membershipTemplate: SessionMembershipData = {
call_id: "", "call_id": "",
scope: "m.room", "scope": "m.room",
application: "m.call", "application": "m.call",
device_id: "AAAAAAA", "device_id": "AAAAAAA",
focus_active: { type: "livekit" }, "focus_active": { type: "livekit", focus_selection: "oldest_membership" },
foci_preferred: [{ type: "livekit" }], "foci_preferred": [{ type: "livekit" }],
"m.call.intent": "voice",
}; };
it("rejects membership with no device_id", () => { it("rejects membership with no device_id", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined })); createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
}).toThrow(); }).toThrow();
}); });
it("rejects membership with no call_id", () => { it("rejects membership with no call_id", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined })); createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
}).toThrow(); }).toThrow();
}); });
it("allow membership with no scope", () => { it("allow membership with no scope", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined })); createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
}).not.toThrow(); }).not.toThrow();
}); });
it("uses event timestamp if no created_ts", () => { 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); expect(membership.createdTs()).toEqual(12345);
}); });
it("uses created_ts if present", () => { it("uses created_ts if present", () => {
const membership = new CallMembership( const membership = createCallMembership(
makeMockEvent(12345), makeMockEvent(12345),
Object.assign({}, membershipTemplate, { created_ts: 67890 }), Object.assign({}, membershipTemplate, { created_ts: 67890 }),
); );
@@ -81,47 +87,347 @@ describe("CallMembership", () => {
it("considers memberships unexpired if local age low enough", () => { it("considers memberships unexpired if local age low enough", () => {
const fakeEvent = makeMockEvent(1000); const fakeEvent = makeMockEvent(1000);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1)); fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false); expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
}); });
it("considers memberships expired if local age large enough", () => { it("considers memberships expired if local age large enough", () => {
const fakeEvent = makeMockEvent(1000); const fakeEvent = makeMockEvent(1000);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1)); fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true); expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
}); });
it("returns preferred foci", () => { it("returns preferred foci", () => {
const fakeEvent = makeMockEvent(); const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" }; const mockFocus = { type: "this_is_a_mock_focus" };
const membership = new CallMembership( const membership = createCallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
fakeEvent, expect(membership.transports).toEqual([mockFocus]);
Object.assign({}, membershipTemplate, { foci_preferred: [mockFocus] }), });
);
expect(membership.getPreferredFoci()).toEqual([mockFocus]); describe("getTransport", () => {
const mockFocus = { type: "this_is_a_mock_focus" };
const oldestMembership = createCallMembership(makeMockEvent(), membershipTemplate);
it("gets the correct active transport with oldest_membership", () => {
const membership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
});
// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual(mockFocus);
// If there is an older member we use its focus.
expect(membership.getTransport(oldestMembership)).toBe(membershipTemplate.foci_preferred[0]);
});
it("gets the correct active transport with multi_sfu", () => {
const membership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "multi_sfu" },
});
// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual(mockFocus);
// If there is an older member we still use our own focus in multi sfu.
expect(membership.getTransport(oldestMembership)).toBe(mockFocus);
});
it("does not provide focus if the selection method is unknown", () => {
const membership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "unknown" },
});
// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toBeUndefined();
});
});
describe("correct values from computed fields", () => {
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org");
});
it("returns correct eventId", () => {
expect(membership.eventId).toBe("$eventid");
});
it("returns correct slot_id", () => {
// 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");
});
it("returns correct call intent", () => {
expect(membership.callIntent).toBe("voice");
});
it("returns correct application", () => {
expect(membership.application).toStrictEqual("m.call");
});
it("returns correct applicationData", () => {
expect(membership.applicationData).toStrictEqual({ "type": "m.call", "m.call.intent": "voice" });
});
it("returns correct scope", () => {
expect(membership.scope).toBe("m.room");
});
it("returns correct membershipID", () => {
expect(membership.membershipID).toBe("@alice:example.org:AAAAAAA");
});
it("returns correct unused fields", () => {
expect(membership.getAbsoluteExpiry()).toBe(DEFAULT_EXPIRE_DURATION);
expect(membership.getMsUntilExpiry()).toBe(DEFAULT_EXPIRE_DURATION - Date.now());
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("expiry calculation", () => { describe("RtcMembershipData", () => {
let fakeEvent: MatrixEvent; function makeMockEvent(originTs = 0, content: IContent = {}): MatrixEvent {
let membership: CallMembership; 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" },
member: { user_id: "@alice:example.org", device_id: "AAAAAAA", id: "xyzHASHxyz" },
rtc_transports: [{ type: "livekit" }],
versions: [],
msc4354_sticky_key: "abc123",
};
beforeEach(() => { it("rejects membership with no slot_id", () => {
// server origin timestamp for this event is 1000 expect(() => {
fakeEvent = makeMockEvent(1000); createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
membership = new CallMembership(fakeEvent!, membershipTemplate); }).toThrow();
});
jest.useFakeTimers(); it("rejects membership with invalid slot_id", () => {
expect(() => {
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(() => {
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#" });
}).not.toThrow();
}); });
afterEach(() => { it("rejects membership with no application", () => {
jest.useRealTimers(); expect(() => {
createCallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
}).toThrow();
}); });
it("calculates time until expiry", () => { it("rejects membership with incorrect application", () => {
jest.setSystemTime(2000); expect(() => {
// should be using absolute expiry time createCallMembership(makeMockEvent(), {
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000); ...membershipTemplate,
application: { wrong_type_key: "unknown" },
});
}).toThrow();
});
it("rejects membership with no member", () => {
expect(() => {
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
}).toThrow();
});
it("rejects membership with incorrect member", () => {
expect(() => {
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
}).toThrow();
expect(() => {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id_wrong: "test" },
});
}).toThrow();
expect(() => {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" },
});
}).toThrow();
expect(() => {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id: "@@test" },
});
}).toThrow();
expect(() => {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id: "@test-wrong-user:user.id" },
});
}).toThrow();
});
it("rejects membership with incorrect sticky_key", () => {
expect(() => {
createCallMembership(makeMockEvent(), membershipTemplate);
}).not.toThrow();
expect(() => {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
sticky_key: 1,
msc4354_sticky_key: undefined,
});
}).toThrow();
expect(() => {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
sticky_key: "1",
msc4354_sticky_key: undefined,
});
}).not.toThrow();
expect(() => {
createCallMembership(makeMockEvent(), { ...membershipTemplate, msc4354_sticky_key: undefined });
}).toThrow();
expect(() => {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
msc4354_sticky_key: 1,
sticky_key: "valid",
});
}).toThrow();
expect(() => {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
msc4354_sticky_key: "valid",
sticky_key: "valid",
});
}).not.toThrow();
expect(() => {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
msc4354_sticky_key: "valid_but_different",
sticky_key: "valid",
});
}).toThrow();
});
// 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 = createCallMembership(makeMockEvent(), {
...membershipTemplate,
rtc_transports: [{ type: "oldest_transport" }],
});
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });
// If there is an older member we use our own focus focus. (RtcMembershipData always uses multi sfu)
expect(membership.getTransport(oldestMembership)).toStrictEqual({ type: "livekit" });
});
});
describe("correct values from computed fields", () => {
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org");
});
it("returns correct eventId", () => {
expect(membership.eventId).toBe("$eventid");
});
it("returns correct slot_id", () => {
expect(membership.slotId).toBe("m.call#");
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
});
it("returns correct deviceId", () => {
expect(membership.deviceId).toBe("AAAAAAA");
});
it("returns correct call intent", () => {
expect(membership.callIntent).toBe("voice");
});
it("returns correct application", () => {
expect(membership.application).toStrictEqual("m.call");
});
it("returns correct applicationData", () => {
expect(membership.applicationData).toStrictEqual({
"type": "m.call",
"m.call.id": "",
"m.call.intent": "voice",
});
});
it("returns correct scope", () => {
expect(membership.scope).toBe(undefined);
});
it("returns correct membershipID", () => {
expect(membership.membershipID).toBe("xyzHASHxyz");
});
it("returns correct unused fields", () => {
expect(membership.getAbsoluteExpiry()).toBe(undefined);
expect(membership.getMsUntilExpiry()).toBe(undefined);
expect(membership.isExpired()).toBe(false);
});
});
it("uses unpadded base64 for RTC backend identities", async () => {
const membership = await CallMembership.parseFromEvent(makeMockEvent(0, { ...membershipTemplate }));
expect(membership.rtcBackendIdentity).toBe("jUZ0Q1yF5nV3LlAI5xfD1I7BPnAytJaPEAR57EXjJ6s");
}); });
}); });
}); });
@@ -14,47 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { isLivekitFocus, isLivekitFocusActive, isLivekitFocusConfig } from "../../../src/matrixrtc/LivekitFocus"; import {
isLivekitTransport,
isLivekitFocusSelection,
isLivekitTransportConfig,
} from "../../../src/matrixrtc/LivekitTransport";
describe("LivekitFocus", () => { describe("LivekitFocus", () => {
it("isLivekitFocus", () => { it("isLivekitFocus", () => {
expect( expect(
isLivekitFocus({ isLivekitTransport({
type: "livekit", type: "livekit",
livekit_service_url: "http://test.com", livekit_service_url: "http://test.com",
livekit_alias: "test", livekit_alias: "test",
}), }),
).toBeTruthy(); ).toBeTruthy();
expect(isLivekitFocus({ type: "livekit" })).toBeFalsy(); expect(isLivekitTransport({ type: "livekit" })).toBeFalsy();
expect( expect(
isLivekitFocus({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }), isLivekitTransport({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }),
).toBeFalsy(); ).toBeFalsy();
expect( expect(
isLivekitFocus({ type: "livekit", other_service_url: "http://test.com", livekit_alias: "test" }), isLivekitTransport({ type: "livekit", other_service_url: "http://test.com", livekit_alias: "test" }),
).toBeFalsy(); ).toBeFalsy();
expect( expect(
isLivekitFocus({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }), isLivekitTransport({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }),
).toBeFalsy(); ).toBeFalsy();
}); });
it("isLivekitFocusActive", () => { it("isLivekitFocusActive", () => {
expect( expect(
isLivekitFocusActive({ isLivekitFocusSelection({
type: "livekit", type: "livekit",
focus_selection: "oldest_membership", focus_selection: "oldest_membership",
}), }),
).toBeTruthy(); ).toBeTruthy();
expect(isLivekitFocusActive({ type: "livekit" })).toBeFalsy(); expect(isLivekitFocusSelection({ type: "livekit" })).toBeFalsy();
expect(isLivekitFocusActive({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy(); expect(isLivekitFocusSelection({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
}); });
it("isLivekitFocusConfig", () => { it("isLivekitFocusConfig", () => {
expect( expect(
isLivekitFocusConfig({ isLivekitTransportConfig({
type: "livekit", type: "livekit",
livekit_service_url: "http://test.com", livekit_service_url: "http://test.com",
}), }),
).toBeTruthy(); ).toBeTruthy();
expect(isLivekitFocusConfig({ type: "livekit" })).toBeFalsy(); expect(isLivekitTransportConfig({ type: "livekit" })).toBeFalsy();
expect(isLivekitFocusConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy(); expect(isLivekitTransportConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
expect(isLivekitFocusConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy(); expect(isLivekitTransportConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
}); });
}); });
File diff suppressed because it is too large Load Diff
@@ -14,54 +14,174 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src"; import { ClientEvent, EventTimeline, MatrixClient, type Room, RoomStateEvent } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state"; import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc";
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager"; import {
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks"; 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("MatrixRTCSessionManager", () => { describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
let client: MatrixClient; "MatrixRTCSessionManager ($eventKind)",
({ eventKind }) => {
let client: MatrixClient;
beforeEach(() => { function generateMembership(opts: { type: string; callId?: string } = { type: "m.call" }): MembershipData {
client = new MatrixClient({ baseUrl: "base_url" }); if (eventKind === "sticky") {
client.matrixRTC.start(); return {
}); ...rtcMembershipTemplate,
slot_id: opts.callId ? `${opts.type}#${opts.callId}` : rtcMembershipTemplate.slot_id,
application: {
...rtcMembershipTemplate.application,
type: opts.type,
},
} satisfies RtcMembershipData & { user_id: string };
}
afterEach(() => { return {
client.stopClient(); ...sessionMembershipTemplate,
client.matrixRTC.stop(); application: opts.type,
}); call_id: opts.callId ?? sessionMembershipTemplate.call_id, // approximate version.
} satisfies SessionMembershipData & { user_id: string };
}
it("Fires event when session starts", () => { async function sendLeaveMembership(room: Room, membershipData: MembershipData[]): Promise<void> {
const onStarted = jest.fn(); if (eventKind === "memberState") {
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted); 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: sessionMembershipTemplate.user_id });
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
}
await flushPromises();
}
try { beforeEach(() => {
const room1 = makeMockRoom([membershipTemplate]); client = new MatrixClient({ baseUrl: "base_url" });
jest.spyOn(client, "getRooms").mockReturnValue([room1]); client.matrixRTC.start();
});
afterEach(() => {
client.stopClient();
client.matrixRTC.stop();
vi.resetAllMocks();
});
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 = vi.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
try {
const room1 = makeMockRoom([generateMembership({ type: "m.other" })], eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
}
});
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");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
vi.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
await sessionStartedPromise;
await sendLeaveMembership(room1, membershipData);
await expect(sessionEndedPromise).resolves.toStrictEqual([
room1.roomId,
client.matrixRTC.getActiveRoomSession(room1),
]);
});
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",
application: "m.notCall",
});
// manually start the session manager (its not the default one started by the client)
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)),
);
// 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 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. Not tracked.
vi.spyOn(client, "getRoom").mockReturnValue(room1);
await sendLeaveMembership(room1, room1MembershipData);
expect(onEnded).not.toHaveBeenCalled();
// 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", async () => {
const onEnded = vi.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const membership: MembershipData[] = [generateMembership({ type: "m.other_app" })];
const room1 = makeMockRoom(membership, eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
vi.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1); client.emit(ClientEvent.Room, room1);
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
}
});
it("Fires event when session ends", () => { await sendLeaveMembership(room1, membership);
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1); expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]); },
);
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
});
@@ -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");
});
});
File diff suppressed because it is too large Load Diff

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