Compare commits

..

115 Commits

Author SHA1 Message Date
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
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
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
209 changed files with 9488 additions and 8310 deletions
+28 -12
View File
@@ -1,6 +1,6 @@
module.exports = {
plugins: ["matrix-org", "import", "jsdoc", "n"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
plugins: ["matrix-org", "import", "jsdoc", "n", "@vitest"],
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
parserOptions: {
project: ["./tsconfig.json"],
},
@@ -83,16 +83,6 @@ module.exports = {
],
},
],
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"jest/no-disabled-tests": "off",
// Used in some crypto tests.
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
},
],
},
overrides: [
{
@@ -147,11 +137,37 @@ module.exports = {
},
{
files: ["spec/**/*.ts"],
extends: ["plugin:@vitest/legacy-recommended"],
rules: {
// We don't need super strict typing in test utilities
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-empty-object-type": "off",
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"@vitest/no-disabled-tests": "off",
// Used in some crypto tests.
"@vitest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
},
],
"@vitest/expect-expect": [
"error",
{
assertFunctionNames: [
"expect",
"expectDevices",
"assert.isTrue",
"assert.isFalse",
"passwordTest",
"compareHeaders",
"doTest",
],
},
],
},
},
{
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@59cbc563d11314e48122193f8fe5cdda62ea6cf9 # v1
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@59cbc563d11314e48122193f8fe5cdda62ea6cf9 # v1
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@59cbc563d11314e48122193f8fe5cdda62ea6cf9 # v1
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+3
View File
@@ -41,3 +41,6 @@
- name: "Z-Flaky-Test"
description: "A test is raising false alarms"
color: "ededed"
- name: "Z-Skip-Coverage"
description: "Skip SonarQube coverage for this PR"
color: "ededed"
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
deployments: write
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
+2
View File
@@ -73,6 +73,8 @@ jobs:
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-24.04
permissions:
pull-requests: write
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
@@ -16,12 +16,12 @@ jobs:
contents: write
steps:
- name: 🧮 Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
ref: staging
fetch-depth: 0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
node-version-file: package.json
cache: "yarn"
@@ -37,7 +37,7 @@ jobs:
disable-autolabeler: true
- name: Get actions scripts
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
+3 -3
View File
@@ -18,14 +18,14 @@ jobs:
merge:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
- name: Get actions scripts
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
@@ -33,7 +33,7 @@ jobs:
sparse-checkout: |
scripts/release
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version-file: package.json
+3 -7
View File
@@ -4,8 +4,6 @@ on:
secrets:
ELEMENT_BOT_TOKEN:
required: true
NPM_TOKEN:
required: false
GPG_PASSPHRASE:
required: false
GPG_PRIVATE_KEY:
@@ -73,7 +71,7 @@ jobs:
draft: true
latest: true
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
ref: staging
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
@@ -81,7 +79,7 @@ jobs:
fetch-depth: 0
- name: Get actions scripts
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
@@ -125,7 +123,7 @@ jobs:
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version-file: package.json
@@ -282,8 +280,6 @@ jobs:
permissions:
contents: read
id-token: write
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
post-release:
name: Post release steps
+8 -14
View File
@@ -1,9 +1,6 @@
name: Publish to npm
on:
workflow_call:
secrets:
NPM_TOKEN:
required: true
outputs:
id:
description: "The npm package@version string we published"
@@ -20,32 +17,29 @@ jobs:
id: ${{ steps.npm-publish.outputs.id }}
steps:
- name: 🧮 Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
ref: staging
- name: 🔧 Yarn cache
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
registry-url: "https://registry.npmjs.org"
node-version-file: package.json
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
run: |
npm publish --provenance --access public --tag next
npm publish --provenance --access public --tag "$TAG"
release=$(jq -r '"\(.name)@\(.version)"' package.json)
echo "id=$release" >> $GITHUB_OUTPUT
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- 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 }}
TAG: ${{ contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest' }}
+4 -4
View File
@@ -44,13 +44,13 @@ jobs:
repo:
- element-hq/element-web
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: ${{ matrix.repo }}
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
@@ -73,10 +73,10 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: 🧮 Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: 🔧 Yarn cache
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version-file: package.json
+6 -5
View File
@@ -12,7 +12,7 @@ on:
sharded:
type: boolean
required: false
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
permissions: {}
jobs:
sonarqube:
@@ -36,14 +36,14 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: 📥 Download artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
if: ${{ !inputs.sharded }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -51,7 +51,7 @@ jobs:
name: coverage
path: coverage
- name: 📥 Download sharded artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
if: inputs.sharded
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -75,13 +75,14 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@820f7c2e9e94ba9e35add0f739691e5c7e23fa25 # v4.0
uses: matrix-org/sonarcloud-workflow-action@9f6f057c1b30500877460c66d4ae937e53d168ee
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
skip_checkout: true
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
skip_coverage_label: Z-Skip-Coverage
version_cmd: "cat package.json | jq -r .version"
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
+16 -16
View File
@@ -14,9 +14,9 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version-file: package.json
@@ -31,9 +31,9 @@ jobs:
name: "ESLint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version-file: package.json
@@ -48,9 +48,9 @@ jobs:
name: "Node.js example"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version-file: package.json
@@ -61,7 +61,7 @@ jobs:
- name: Build Types
run: "yarn build:types"
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "npm"
node-version-file: "examples/node/package.json"
@@ -83,9 +83,9 @@ jobs:
name: "Workflow Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version-file: package.json
@@ -100,9 +100,9 @@ jobs:
name: "JSDoc Checker"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version-file: package.json
@@ -114,7 +114,7 @@ jobs:
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: docs
path: _docs
@@ -125,9 +125,9 @@ jobs:
name: "Analyse Dead Code"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version-file: package.json
@@ -143,11 +143,11 @@ jobs:
if: github.event_name == 'merge_group'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
repository: element-hq/element-web
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: "lts/*"
+11 -15
View File
@@ -12,8 +12,8 @@ env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
permissions: {} # No permissions required
jobs:
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
test:
name: "Vitest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
@@ -22,11 +22,11 @@ jobs:
node: ["lts/*", 22]
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Setup Node
id: setupNode
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache: "yarn"
node-version: ${{ matrix.node }}
@@ -42,14 +42,10 @@ jobs:
run: |
yarn test \
--coverage=${{ env.ENABLE_COVERAGE }} \
--ci \
--max-workers ${{ steps.cpu-cores.outputs.count }} \
--maxWorkers ${{ steps.cpu-cores.outputs.count }} \
./spec/${{ matrix.specs }}
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
# tell jest to use coloured output
FORCE_COLOR: true
SHARD: ${{ matrix.specs }}
- name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true'
@@ -57,7 +53,7 @@ jobs:
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
path: |
@@ -65,13 +61,13 @@ jobs:
!coverage/lcov-report
# Dummy completion job to simplify branch protections
jest-complete:
name: Jest tests
needs: jest
complete:
name: Tests
needs: test
if: always()
runs-on: ubuntu-24.04
steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
- if: needs.test.result != 'skipped' && needs.test.result != 'success'
run: exit 1
element-web:
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
with:
operations-per-run: 250
days-before-issue-stale: -1
+97
View File
@@ -1,3 +1,100 @@
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
+1 -1
View File
@@ -117,7 +117,7 @@ checks, so please check back after a few minutes.
Your PR should include tests.
For new user facing features in `matrix-js-sdk`, you
must include comprehensive unit tests written in Jest.
must include comprehensive unit tests written in Vitest.
The existing tests can be found under `spec/unit`
It's good practice to write tests alongside the code as it ensures the code is testable from
+1 -1
View File
@@ -464,7 +464,7 @@ To build a browser version from scratch when developing:
$ yarn build
```
To run tests (Jest):
To run tests:
```
$ yarn test
+2 -10
View File
@@ -7,21 +7,13 @@ module.exports = {
targets: {
esmodules: true,
},
// We want to output ES modules for the final build (mostly to ensure that
// async imports work correctly). However, jest doesn't support ES modules very
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
// when testing.
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
modules: false,
},
],
[
"@babel/preset-typescript",
{
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import`
// specifiers. However, Jest uses the TS source (via babel) and fails to resolve the `.js` names.
// To resolve this,we use the `.ts` names in the source, and rewrite the `import` specifiers to use
// `.js` during transpilation, *except* when we are targetting Jest.
rewriteImportExtensions: process.env.NODE_ENV !== "test",
rewriteImportExtensions: true,
},
],
],
+4 -5
View File
@@ -71,7 +71,7 @@ Unless otherwise specified, the following applies to all code:
11. If a variable is not receiving a value on declaration, its type must be defined.
```typescript
let errorMessage: Optional<string>;
let errorMessage: string;
```
12. Objects can use shorthand declarations, including mixing of types.
@@ -150,8 +150,7 @@ Unless otherwise specified, the following applies to all code:
1. When using `any`, a comment explaining why must be present.
27. `import` should be used instead of `require`, as `require` does not have types.
28. Export only what can be reused.
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
of truly optional parameters.
29. Prefer a type like `X | null` instead of truly optional parameters.
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
takes an argument that is more often not required than required. An example where the
`?` operator is inappropriate is when taking a room ID: typically the caller should
@@ -161,7 +160,7 @@ Unless otherwise specified, the following applies to all code:
```typescript
function doThingWithRoom(
thing: string,
room: Optional<string>, // require the caller to specify
room: string | null, // require the caller to specify
) {
// ...
}
@@ -214,7 +213,7 @@ Unless otherwise specified, the following applies to all code:
## Tests
1. Tests must be written in TypeScript.
2. Jest mocks are declared below imports, but above everything else.
2. Mocks are declared below imports, but above everything else.
3. Use the following convention template:
```typescript
-48
View File
@@ -1,48 +0,0 @@
/* Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type { Config } from "jest";
import { env } from "process";
const config: Config = {
testEnvironment: "node",
testMatch: ["<rootDir>/spec/**/*.spec.{js,ts}"],
setupFilesAfterEnv: ["<rootDir>/spec/setupTests.ts"],
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts}"],
coverageReporters: ["text-summary", "lcov"],
testResultsProcessor: "@casualbot/jest-sonar-reporter",
transformIgnorePatterns: ["/node_modules/(?!(uuid|p-retry|is-network-error)).+$"],
// Always print out a summary if there are any failing tests. Normally
// a summary is only printed if there are more than 20 test *suites*.
reporters: [["default", { summaryThreshold: 0 }]],
};
// if we're running under GHA, enable the GHA reporter
if (env["GITHUB_ACTIONS"] !== undefined) {
const reporters: Config["reporters"] = [
["github-actions", { silent: false }],
// as above: always show a summary if there were any failing tests.
["summary", { summaryThreshold: 0 }],
];
// if we're running against the develop branch, also enable the slow test reporter
if (env["GITHUB_REF"] == "refs/heads/develop") {
reporters.push("<rootDir>/spec/slowReporter.cjs");
}
config.reporters = reporters;
}
export default config;
+2 -4
View File
@@ -28,10 +28,8 @@ export default {
"husky",
// Used in script which only runs in environment with `@octokit/rest` installed
"@octokit/rest",
// Used by jest
"jest-environment-jsdom",
"babel-jest",
"ts-node",
// Used by `vitest`
"vitest-sonar-reporter",
// Used by `@babel/plugin-transform-runtime`
"@babel/runtime",
],
+21 -27
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "39.0.0",
"version": "40.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=22.0.0"
@@ -19,8 +19,8 @@
"lint:types": "tsc --noEmit",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip",
"test": "jest",
"test:watch": "jest --watch",
"test": "vitest",
"test:watch": "vitest --watch",
"coverage": "yarn test --coverage"
},
"repository": {
@@ -49,17 +49,17 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^15.3.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^17.0.0",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
"content-type": "^1.0.4",
"jwt-decode": "^4.0.0",
"loglevel": "^1.9.2",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.10.0",
"matrix-widget-api": "^1.14.0",
"oidc-client-ts": "^3.0.1",
"p-retry": "7",
"sdp-transform": "^2.14.1",
"sdp-transform": "^3.0.0",
"unhomoglyph": "^1.0.6",
"uuid": "13"
},
@@ -78,18 +78,19 @@
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@fetch-mock/vitest": "^0.2.18",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
"@stylistic/eslint-plugin": "^5.0.0",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
"@types/jest": "^29.0.0",
"@types/node": "18",
"@types/sdp-transform": "^2.4.5",
"@typescript-eslint/eslint-plugin": "^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",
"debug": "^4.3.4",
"eslint": "8.57.1",
@@ -97,36 +98,29 @@
"eslint-config-prettier": "^10.0.0",
"eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^28.0.0",
"eslint-plugin-jsdoc": "^50.0.0",
"eslint-plugin-matrix-org": "2.1.0",
"eslint-plugin-jsdoc": "^61.0.0",
"eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-n": "^14.0.0",
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-tsdoc": "^0.5.0",
"eslint-plugin-unicorn": "^56.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "11.1.5",
"fetch-mock-jest": "^1.5.1",
"fetch-mock": "^12.6.0",
"happy-dom": "^20.1.0",
"husky": "^9.0.0",
"jest": "^29.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",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.6.2",
"prettier": "3.7.4",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"typedoc": "^0.28.1",
"typedoc-plugin-coverage": "^4.0.0",
"typedoc-plugin-mdn-links": "^5.0.0",
"typedoc-plugin-missing-exports": "^4.0.0",
"typescript": "^5.4.2"
"typescript": "^5.4.2",
"vitest": "^4.0.17",
"vitest-sonar-reporter": "^3.0.0"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
"resolutions": {
"expect": "30.2.0"
}
}
+1 -1
View File
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
sonar.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=spec/**/*
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
sonar.testExecutionReportPaths=coverage/sonar-report.xml
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
+2 -2
View File
@@ -17,7 +17,7 @@ limitations under the License.
*/
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
/* eslint-disable jest/no-standalone-expect */
/* eslint-disable @vitest/no-standalone-expect */
import MockHttpBackend from "matrix-mock-request";
@@ -39,7 +39,7 @@ import { type ISyncResponder } from "./test-utils/SyncResponder";
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
* Instead, construct a MatrixClient manually, use fetch-mock to intercept the HTTP requests, and
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
*/
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
+29 -47
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import debug from "debug";
@@ -74,7 +74,7 @@ describe("cross-signing", () => {
function createCryptoCallbacks(): CryptoCallbacks {
return {
getSecretStorageKey: (keys, name) => {
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
return Promise.resolve<[string, Uint8Array<ArrayBuffer>]>(["key_id", encryptionKey]);
},
};
}
@@ -83,7 +83,6 @@ describe("cross-signing", () => {
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
@@ -113,8 +112,7 @@ describe("cross-signing", () => {
);
afterEach(async () => {
await aliceClient.stopClient();
fetchMock.mockReset();
aliceClient.stopClient();
});
/**
@@ -138,27 +136,25 @@ describe("cross-signing", () => {
await bootstrapCrossSigning(authDict);
// check that the cross-signing keys have been uploaded
expect(fetchMock.called("upload-cross-signing-keys")).toBeTruthy();
const [, keysOpts] = fetchMock.lastCall("upload-cross-signing-keys")!;
expect(fetchMock.callHistory.called("upload-cross-signing-keys")).toBeTruthy();
const keysOpts = fetchMock.callHistory.lastCall("upload-cross-signing-keys")!.options;
const keysBody = JSON.parse(keysOpts!.body as string);
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
// there should be a key of each type
// master key is signed by the device
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
expect(keysBody).toHaveProperty(["master_key", "signatures", TEST_USER_ID, `ed25519:${TEST_DEVICE_ID}`]);
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
// ssk and usk are signed by the master key
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
expect(keysBody).toHaveProperty(["self_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
expect(keysBody).toHaveProperty(["user_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
// check the publish call
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
const body = JSON.parse(sigsOpts!.body as string);
// there should be a signature for our device, by our self-signing key.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
);
expect(body).toHaveProperty([TEST_USER_ID, TEST_DEVICE_ID, "signatures", TEST_USER_ID, sskId]);
});
it("get cross signing keys from secret storage and import them", async () => {
@@ -237,13 +233,17 @@ describe("cross-signing", () => {
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
// Expect the signature to be uploaded
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
const body = JSON.parse(sigsOpts!.body as string);
// the device should have a signature with the public self cross signing keys.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
);
expect(body).toHaveProperty([
TEST_USER_ID,
TEST_DEVICE_ID,
"signatures",
TEST_USER_ID,
`ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}`,
]);
});
it("can bootstrapCrossSigning twice", async () => {
@@ -255,8 +255,7 @@ describe("cross-signing", () => {
// a second call should do nothing except GET requests
fetchMock.mockClear();
await bootstrapCrossSigning(authDict);
const calls = fetchMock.calls((url, opts) => opts.method != "GET");
expect(calls.length).toEqual(0);
expect(fetchMock).toHaveFetchedTimes(0, "unmatched");
});
it("will upload existing cross-signing keys to an established secret storage", async () => {
@@ -267,7 +266,6 @@ describe("cross-signing", () => {
// To arrange that, we call `bootstrapCrossSigning` on our main device, and then (pretend to) set up 4S from
// a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
mockSetupCrossSigningRequests();
const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
accountDataAccumulator.interceptGetAccountData();
@@ -282,7 +280,7 @@ describe("cross-signing", () => {
});
// Prepare for the cross-signing keys
const p = accountDataAccumulator.interceptSetAccountData(":type(m.cross_signing..*)");
const p = accountDataAccumulator.waitForAccountData("m.cross_signing.master");
await bootstrapCrossSigning(authDict);
await p;
@@ -403,7 +401,7 @@ describe("cross-signing", () => {
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeFalsy();
});
}, 10000);
});
describe("getCrossSigningKeyId", () => {
@@ -415,22 +413,13 @@ describe("cross-signing", () => {
*/
function awaitCrossSigningKeysUpload() {
return new Promise<any>((resolve) => {
fetchMock.post(
{
url: new URL(
"/_matrix/client/v3/keys/device_signing/upload",
aliceClient.getHomeserverUrl(),
).toString(),
name: "upload-cross-signing-keys",
},
(url, options) => {
const content = JSON.parse(options.body as string);
fetchMock.modifyRoute("upload-cross-signing-keys", {
response: (callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return {};
},
// Override the route defined in E2EKeyReceiver
{ overwriteRoutes: true },
);
});
});
}
@@ -461,9 +450,6 @@ describe("cross-signing", () => {
describe("crossSignDevice", () => {
beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
// make sure that there is another device which we can sign
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
@@ -477,10 +463,6 @@ describe("cross-signing", () => {
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
});
afterEach(async () => {
jest.useRealTimers();
});
it("fails for an unknown device", async () => {
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
});
@@ -493,9 +475,9 @@ describe("cross-signing", () => {
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
// check that a sig for the device was uploaded
const calls = fetchMock.calls("upload-sigs");
const calls = fetchMock.callHistory.calls("upload-sigs");
expect(calls.length).toEqual(1);
const body = JSON.parse(calls[0][1]!.body as string);
const body = JSON.parse(calls[0].options!.body as string);
const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
expect(deviceSig).toHaveProperty("signatures");
});
+93 -127
View File
@@ -16,12 +16,12 @@ limitations under the License.
*/
import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import Olm from "@matrix-org/olm";
import { type RouteResponse } from "fetch-mock";
import type FetchMock from "fetch-mock";
import * as testUtils from "../../test-utils/test-utils";
import {
emitPromise,
@@ -104,7 +104,7 @@ afterEach(() => {
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
jest.useRealTimers();
vi.useRealTimers();
});
describe("crypto", () => {
@@ -154,13 +154,8 @@ describe("crypto", () => {
return response;
}
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
fetchMock.postOnce(
new RegExp(rootRegexp + "(r0|v3)/keys/query"),
(url: string, opts: RequestInit) => onQueryRequest(JSON.parse(opts.body as string)),
{
// append to the list of intercepts on this path
overwriteRoutes: false,
},
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/query"), (callLog) =>
onQueryRequest(JSON.parse(callLog.options.body as string)),
);
}
@@ -170,7 +165,7 @@ describe("crypto", () => {
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
* (or a function that returns one).
*/
function expectAliceKeyClaim(response: FetchMock.MockResponse | FetchMock.MockResponseFunction) {
function expectAliceKeyClaim(response: RouteResponse) {
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
}
@@ -223,15 +218,20 @@ describe("crypto", () => {
*/
function createCryptoCallbacks(): CryptoCallbacks {
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
let cachedKey: { keyId: string; key: Uint8Array };
const cacheSecretStorageKey = (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => {
let cachedKey: { keyId: string; key: Uint8Array<ArrayBuffer> };
const cacheSecretStorageKey = (
keyId: string,
keyInfo: SecretStorageKeyDescription,
key: Uint8Array<ArrayBuffer>,
) => {
cachedKey = {
keyId,
key,
};
};
const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);
const getSecretStorageKey = () =>
Promise.resolve<[string, Uint8Array<ArrayBuffer>]>([cachedKey.keyId, cachedKey.key]);
return {
cacheSecretStorageKey,
@@ -243,7 +243,6 @@ describe("crypto", () => {
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
@@ -265,18 +264,20 @@ describe("crypto", () => {
testOlmAccount = await createOlmAccount();
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
vi.useRealTimers();
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
);
afterEach(async () => {
await aliceClient.stopClient();
aliceClient.stopClient();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
if (vi.isFakeTimers()) {
await vi.runAllTimersAsync();
}
});
it("MatrixClient.getCrypto returns a CryptoApi", () => {
@@ -343,7 +344,7 @@ describe("crypto", () => {
describe("Unable to decrypt error codes", function () {
beforeEach(() => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
});
it("Decryption fails with UISI error", async () => {
@@ -719,7 +720,7 @@ describe("crypto", () => {
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
await awaitDecryptionError;
await expect(awaitDecryptionError).resolves.toBeUndefined();
});
});
@@ -870,6 +871,7 @@ describe("crypto", () => {
await expectSendRoomKey("@bob:xyz", testOlmAccount);
});
// eslint-disable-next-line @vitest/expect-expect
it("Alice sends a megolm message", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -897,6 +899,7 @@ describe("crypto", () => {
]);
});
// eslint-disable-next-line @vitest/expect-expect
it("We should start a new megolm session after forceDiscardSession", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -997,9 +1000,7 @@ describe("crypto", () => {
await startClientAndAwaitFirstSync();
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount);
// We need to fake the timers to advance the time, but the wasm bindings of matrix-sdk-crypto rely on a
// working `queueMicrotask`
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
const syncResponse = getSyncResponse(["@bob:xyz"]);
@@ -1031,7 +1032,7 @@ describe("crypto", () => {
expect(sessionId).toBeDefined();
// Advance the time by 1h
jest.advanceTimersByTime(oneHourInMs);
vi.advanceTimersByTime(oneHourInMs);
// Send a second message to bob and get the encrypted message
const [secondEncryptedMessage] = await Promise.all([
@@ -1152,7 +1153,7 @@ describe("crypto", () => {
// it probably won't be decrypted yet, because it takes a while to process the olm keys
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID);
expect(decryptedEvent.getContent()).toEqual({});
expect(decryptedEvent.getContent<IContent>()).toEqual({});
expect(decryptedEvent.getClearContent()).toBeUndefined();
});
@@ -1176,20 +1177,12 @@ describe("crypto", () => {
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
// ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes.
const sendRoomMessageResolvers = Promise.withResolvers<FetchMock.MockResponse>();
const sendRoomMessageResolvers = Promise.withResolvers<RouteResponse>();
const reqProm = new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp("/send/m.room.encrypted/"),
async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => {
resolve(JSON.parse(opts.body as string));
return await sendRoomMessageResolvers.promise;
},
{
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
overwriteRoutes: false,
},
);
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), async (callLog): Promise<RouteResponse> => {
resolve(JSON.parse(callLog.options.body as string));
return await sendRoomMessageResolvers.promise;
});
});
// Now we start to send the message
@@ -1272,6 +1265,7 @@ describe("crypto", () => {
});
}
// eslint-disable-next-line @vitest/expect-expect
it("Sending an event initiates a member list sync", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -1295,6 +1289,7 @@ describe("crypto", () => {
await Promise.all([sendPromise, megolmMessagePromise, memberListPromise]);
});
// eslint-disable-next-line @vitest/expect-expect
it("loading the membership list inhibits a later load", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
@@ -1404,33 +1399,26 @@ describe("crypto", () => {
describe("key upload request", () => {
beforeEach(() => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
});
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
return new Promise((resolve) => {
const listener = (url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
const keysCount = Object.keys(content?.one_time_keys || {}).length;
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
if (keysCount) resolve({ keysCount, fallbackKeysCount });
return {
one_time_key_counts: {
// The matrix client does `/upload` requests until 50 keys are uploaded
// We return here 60 to avoid the `/upload` request loop
signed_curve25519: keysCount ? 60 : keysCount,
},
};
};
for (const path of ["/_matrix/client/v3/keys/upload", "/_matrix/client/v3/keys/upload"]) {
fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener, {
// These routes are already defined in the E2EKeyReceiver
// We want to overwrite the behaviour of the E2EKeyReceiver
overwriteRoutes: true,
});
}
fetchMock.modifyRoute("keys-upload", {
response: (callLog) => {
const content = JSON.parse(callLog.options.body as string);
const keysCount = Object.keys(content?.one_time_keys || {}).length;
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
if (keysCount) resolve({ keysCount, fallbackKeysCount });
return {
one_time_key_counts: {
// The matrix client does `/upload` requests until 50 keys are uploaded
// We return here 60 to avoid the `/upload` request loop
signed_curve25519: keysCount ? 60 : keysCount,
},
};
},
});
});
}
@@ -1443,7 +1431,7 @@ describe("crypto", () => {
await syncPromise(aliceClient);
// Verify that `/upload` is called on Alice's homesever
// Verify that `/upload` is called on Alice's homeserver
const { keysCount, fallbackKeysCount } = await uploadPromise;
expect(keysCount).toBeGreaterThan(0);
expect(fallbackKeysCount).toBe(0);
@@ -1457,7 +1445,7 @@ describe("crypto", () => {
// Advance local date to 2 minutes
// The old crypto only runs the upload every 60 seconds
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
vi.setSystemTime(Date.now() + 2 * 60 * 1000);
await syncPromise(aliceClient);
@@ -1548,18 +1536,16 @@ describe("crypto", () => {
function awaitKeyQueryRequest(): Promise<Record<string, []>> {
return new Promise((resolve) => {
const listener = (url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
// Resolve with request payload
resolve(content.device_keys);
// Return response of `/keys/query`
return queryResponseBody;
};
fetchMock.post(
new URL("/_matrix/client/v3/keys/query", aliceClient.getHomeserverUrl()).toString(),
listener,
(callLog) => {
const content = JSON.parse(callLog.options.body as string);
// Resolve with request payload
resolve(content.device_keys);
// Return response of `/keys/query`
return queryResponseBody;
},
);
});
}
@@ -1598,8 +1584,7 @@ describe("crypto", () => {
});
it("Get devices from tracked users", async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -1611,12 +1596,12 @@ describe("crypto", () => {
// Advance local date to 2 minutes
// The old crypto only runs the upload every 60 seconds
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
vi.setSystemTime(Date.now() + 2 * 60 * 1000);
await syncPromise(aliceClient);
// Old crypto: for alice: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
jest.runAllTimers();
vi.runAllTimers();
// Old crypto: for alice: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
await flushPromises();
@@ -1624,7 +1609,7 @@ describe("crypto", () => {
await queryPromise;
// Old crypto: for `user`: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
jest.runAllTimers();
vi.runAllTimers();
// Old crypto: for `user`: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
// It will add `@testing_florian1:matrix.org` devices to the DeviceList
await flushPromises();
@@ -1648,7 +1633,7 @@ describe("crypto", () => {
* Create a fake secret storage key
* Async because `bootstrapSecretStorage` expect an async method
*/
const createSecretStorageKey = jest.fn().mockResolvedValue({
const createSecretStorageKey = vi.fn().mockResolvedValue({
keyInfo: {}, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
});
@@ -1666,7 +1651,7 @@ describe("crypto", () => {
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
*/
async function awaitCrossSigningKeyUpload(key: string): Promise<Record<string, {}>> {
const content = await accountDataAccumulator.interceptSetAccountData(`m.cross_signing.${key}`);
const content = await accountDataAccumulator.waitForAccountData(`m.cross_signing.${key}`);
return content.encrypted;
}
@@ -1678,10 +1663,7 @@ describe("crypto", () => {
async function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
// eslint-disable-next-line no-constant-condition
while (true) {
const content = await accountDataAccumulator.interceptSetAccountData(":type(m.secret_storage.*)", {
repeat: 1,
overwriteRoutes: true,
});
const content = await accountDataAccumulator.waitForAccountData("m.secret_storage.*");
if (content.key) {
return content.key;
}
@@ -1689,10 +1671,7 @@ describe("crypto", () => {
}
async function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
const content = await accountDataAccumulator.interceptSetAccountData("m.megolm_backup.v1", {
repeat: 1,
overwriteRoutes: true,
});
const content = await accountDataAccumulator.waitForAccountData("m.megolm_backup.v1");
return content.encrypted;
}
@@ -1713,7 +1692,6 @@ describe("crypto", () => {
* @param backupVersion - The version of the created backup
*/
async function bootstrapSecurity(backupVersion: string): Promise<void> {
mockSetupCrossSigningRequests();
mockSetupMegolmBackupRequests(backupVersion);
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
@@ -1792,6 +1770,7 @@ describe("crypto", () => {
it("Should create a 4S key", async () => {
accountDataAccumulator.interceptGetAccountData();
accountDataAccumulator.interceptSetAccountData();
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
@@ -1943,8 +1922,7 @@ describe("crypto", () => {
describe("Manage Key Backup", () => {
beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
});
it("Should be able to restore from 4S after bootstrap", async () => {
@@ -1961,29 +1939,23 @@ describe("crypto", () => {
const newKey = testData.MEGOLM_SESSION_DATA;
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
resolve(uploadPayload);
return {
status: 200,
body: {
count: 1,
etag: "abcdefg",
},
};
},
{
overwriteRoutes: true,
},
);
fetchMock.put("path:/_matrix/client/v3/room_keys/keys", (callLog) => {
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
resolve(uploadPayload);
return {
status: 200,
body: {
count: 1,
etag: "abcdefg",
},
};
});
});
await aliceClient.getCrypto()!.importRoomKeys([newKey]);
// The backup loop waits a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
vi.runAllTimers();
const keyBackupData = await awaitKeyUploaded;
@@ -2011,40 +1983,33 @@ describe("crypto", () => {
fetchMock.delete(
"express:/_matrix/client/v3/room_keys/version/:version",
(url: string, options: RequestInit) => {
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
fetchMock.modifyRoute("room-keys-version", {
response: {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
},
{ overwriteRoutes: true },
);
});
resolve();
return {};
},
{ overwriteRoutes: true },
);
});
const newVersion = "2";
fetchMock.post(
"path:/_matrix/client/v3/room_keys/version",
(url, request) => {
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
fetchMock.modifyRoute("post-room-keys-version", {
response: (callLog) => {
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
backupData.version = newVersion;
backupData.count = 0;
backupData.etag = "zer";
// update get call with new version
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
overwriteRoutes: true,
});
fetchMock.modifyRoute("room-keys-version", { response: backupData });
return {
version: backupVersion,
};
},
{ overwriteRoutes: true },
);
});
const newBackupStatusUpdate = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
@@ -2106,8 +2071,7 @@ describe("crypto", () => {
});
it("Cross signing keys are available for a tracked user", async () => {
// Process Alice keys, old crypto has a sleep(5ms) during the process
await jest.advanceTimersByTimeAsync(5);
// Process Alice keys
await flushPromises();
// Alice is the local user and should be tracked !
@@ -2195,6 +2159,7 @@ describe("crypto", () => {
client2?.stopClient();
});
// eslint-disable-next-line @vitest/expect-expect
test("Sending a message in a room where the server is hiding the state event does not send a plaintext event", async () => {
// Alice is in an encrypted room
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
@@ -2252,6 +2217,7 @@ describe("crypto", () => {
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
});
// eslint-disable-next-line @vitest/expect-expect
test("Changes to the rotation period should be ignored after a client restart", async () => {
// Alice is in an encrypted room, where the rotation period is set to 2 messages
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
+54 -42
View File
@@ -15,7 +15,8 @@ limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type CallLog } from "fetch-mock";
import debug from "debug";
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
@@ -28,7 +29,7 @@ import { emitPromise, EventCounter } from "../../test-utils/test-utils";
describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -59,28 +60,35 @@ describe("Device dehydration", () => {
});
const crypto = matrixClient.getCrypto()!;
fetchMock.config.overwriteRoutes = true;
// start dehydration -- we start with no dehydrated device, and we
// store the dehydrated device that we create
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
fetchMock.get(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
},
});
{ name: "get-dehydrated-device" },
);
let dehydratedDeviceBody: any;
let dehydrationCount = 0;
let resolveDehydrationPromise: () => void;
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
dehydratedDeviceBody = JSON.parse(opts.body as string);
dehydrationCount++;
if (resolveDehydrationPromise) {
resolveDehydrationPromise();
}
return {};
});
fetchMock.put(
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
(callLog) => {
dehydratedDeviceBody = JSON.parse(callLog.options.body as string);
dehydrationCount++;
if (resolveDehydrationPromise) {
resolveDehydrationPromise();
}
return {};
},
{ name: "put-dehydrated-device" },
);
await crypto.startDehydration();
expect(dehydrationCount).toEqual(1);
@@ -91,7 +99,7 @@ describe("Device dehydration", () => {
const dehydrationPromise = new Promise<void>((resolve, reject) => {
resolveDehydrationPromise = resolve;
});
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise;
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
@@ -101,16 +109,18 @@ describe("Device dehydration", () => {
// restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so
// a new dehydration key will be set
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
fetchMock.modifyRoute("get-dehydrated-device", {
response: {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
},
});
const eventsResponse = jest.fn((url, opts) => {
const eventsResponse = vi.fn((callLog: CallLog) => {
// rehydrating should make two calls to the /events endpoint.
// The first time will return a single event, and the second
// time will return no events (which will signal to the
// rehydration function that it can stop)
const body = JSON.parse(opts.body as string);
const body = JSON.parse(callLog.options.body as string);
const nextBatch = body.next_batch ?? "0";
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
return {
@@ -135,28 +145,30 @@ describe("Device dehydration", () => {
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
// test that if we get an error when we try to rotate, it emits an event
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
fetchMock.modifyRoute("put-dehydrated-device", {
response: {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
},
});
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await rotationErrorEventPromise;
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
fetchMock.modifyRoute("get-dehydrated-device", {
response: {
status: 500,
body: {
errcode: "M_UNKNOWN",
error: "Unknown error",
},
},
});
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
return {};
});
fetchMock.modifyRoute("put-dehydrated-device", { response: { body: {} } });
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
await crypto.startDehydration(true);
await rehydrationErrorEventPromise;
@@ -182,8 +194,8 @@ async function initializeSecretStorage(
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
const accountData: Map<string, object> = new Map();
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
const name = callLog.url.split("/").pop()!;
const value = accountData.get(name);
if (value) {
return value;
@@ -197,9 +209,9 @@ async function initializeSecretStorage(
};
}
});
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
const value = JSON.parse(opts.body as string);
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
const name = callLog.url.split("/").pop()!;
const value = JSON.parse(callLog.options.body as string);
accountData.set(name, value);
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
return {};
+117 -14
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import mkDebug from "debug";
import {
@@ -30,11 +30,12 @@ import {
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver.ts";
import { SyncResponder } from "../../test-utils/SyncResponder.ts";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints.ts";
import { getSyncResponse, mkEventCustom, syncPromise } from "../../test-utils/test-utils.ts";
import { getSyncResponse, mkEventCustom, syncPromise, waitFor } from "../../test-utils/test-utils.ts";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder.ts";
import { flushPromises } from "../../test-utils/flushPromises.ts";
import { E2EOTKClaimResponder } from "../../test-utils/E2EOTKClaimResponder.ts";
import { escapeRegExp } from "../../../src/utils.ts";
import { EventShieldColour, EventShieldReason } from "../../../src/crypto-api";
const debug = mkDebug("matrix-js-sdk:history-sharing");
@@ -82,7 +83,7 @@ describe("History Sharing", () => {
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
mockSetupCrossSigningRequests();
const aliceId = "@alice:localhost";
@@ -128,8 +129,8 @@ describe("History Sharing", () => {
// Now, Alice invites Bob
const uploadProm = new Promise<Uint8Array>((resolve) => {
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (url, request) => {
const body = request.body as Uint8Array;
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (callLog) => {
const body = callLog.options.body as Uint8Array;
debug(`Alice uploaded blob of length ${body.length}`);
resolve(body);
return { content_uri: "mxc://alice-server/here" };
@@ -173,11 +174,9 @@ describe("History Sharing", () => {
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
room_id: ROOM_ID,
});
fetchMock.getOnce(
`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`,
{ body: uploadedBlob },
{ sendAsJson: false },
);
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
body: uploadedBlob,
});
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
// Bob receives, should be able to decrypt, the megolm message
@@ -199,6 +198,110 @@ describe("History Sharing", () => {
await event.getDecryptionPromise();
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().body).toEqual("Hi!");
expect(event.getKeyForwardingUser()).toEqual(aliceClient.getUserId());
const encryptionInfo = await bobClient.getCrypto()!.getEncryptionInfoForEvent(event);
expect(encryptionInfo?.shieldColour).toEqual(EventShieldColour.GREY);
expect(encryptionInfo?.shieldReason).toEqual(EventShieldReason.AUTHENTICITY_NOT_GUARANTEED);
});
test("Room keys are imported correctly if invite is accepted before the bundle arrives", async () => {
// Alice is in an encrypted room
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], ROOM_ID);
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
// ... and she sends an event
const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hello!" });
const sentMessage = await msgProm;
debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
// Now, Alice invites Bob
const uploadProm = new Promise<Uint8Array>((resolve) => {
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (callLog) => {
const body = callLog.options.body as Uint8Array;
debug(`Alice uploaded blob of length ${body.length}`);
resolve(body);
return { content_uri: "mxc://alice-server/here" };
});
});
const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
// POST https://alice-server.com/_matrix/client/v3/rooms/!room%3Aexample.com/invite
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
const uploadedBlob = await uploadProm;
const sentToDeviceRequest = await toDeviceMessageProm;
debug(`Alice sent encrypted to-device events: ${JSON.stringify(sentToDeviceRequest)}`);
const bobToDeviceMessage = sentToDeviceRequest[bobClient.getSafeUserId()][bobClient.deviceId!];
expect(bobToDeviceMessage).toBeDefined();
// Bob receives the room invite, but not the room key bundle
const inviteEvent = mkEventCustom({
type: "m.room.member",
sender: aliceClient.getSafeUserId(),
state_key: bobClient.getSafeUserId(),
content: { membership: KnownMembership.Invite },
});
bobSyncResponder.sendOrQueueSyncResponse({
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [inviteEvent] } } } },
});
await syncPromise(bobClient);
const room = bobClient.getRoom(ROOM_ID);
expect(room).toBeTruthy();
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
room_id: ROOM_ID,
});
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
// Bob receives and attempts to decrypt the megolm message, but should not be able to (yet).
const bobSyncResponse = getSyncResponse([aliceClient.getSafeUserId(), bobClient.getSafeUserId()], ROOM_ID);
bobSyncResponse.rooms.join[ROOM_ID].timeline.events.push(
mkEventCustom({
type: "m.room.encrypted",
sender: aliceClient.getSafeUserId(),
content: sentMessage,
event_id: "$event_id",
}) as any,
);
bobSyncResponder.sendOrQueueSyncResponse(bobSyncResponse);
await syncPromise(bobClient);
const bobRoom = bobClient.getRoom(ROOM_ID);
const event = bobRoom!.getLastLiveEvent()!;
expect(event.getId()).toEqual("$event_id");
await event.getDecryptionPromise();
expect(event.isDecryptionFailure()).toBeTruthy();
// Now the room key bundle message arrives
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
body: uploadedBlob,
});
bobSyncResponder.sendOrQueueSyncResponse({
to_device: {
events: [
{
type: "m.room.encrypted",
sender: aliceClient.getSafeUserId(),
content: bobToDeviceMessage,
},
],
},
});
await syncPromise(bobClient);
// Once the room key bundle finishes downloading, we should be able to decrypt the message.
await waitFor(async () => {
await event.getDecryptionPromise();
expect(event.isDecryptionFailure()).toBeFalsy();
});
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().body).toEqual("Hello!");
expect(event.getKeyForwardingUser()).toEqual(aliceClient.getUserId());
const encryptionInfo = await bobClient.getCrypto()!.getEncryptionInfoForEvent(event);
expect(encryptionInfo?.shieldColour).toEqual(EventShieldColour.GREY);
expect(encryptionInfo?.shieldReason).toEqual(EventShieldReason.AUTHENTICITY_NOT_GUARANTEED);
});
afterEach(async () => {
@@ -212,8 +315,8 @@ function expectSendRoomEvent(homeserverUrl: string, msgtype: string): Promise<IC
return new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp(`^${escapeRegExp(homeserverUrl)}/_matrix/client/v3/rooms/[^/]*/send/${escapeRegExp(msgtype)}/`),
(url, request) => {
const content = JSON.parse(request.body as string);
(callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return { event_id: "$event_id" };
},
@@ -229,8 +332,8 @@ function expectSendToDeviceMessage(
return new Promise((resolve) => {
fetchMock.putOnce(
new RegExp(`^${escapeRegExp(homeserverUrl)}/_matrix/client/v3/sendToDevice/${escapeRegExp(msgtype)}/`),
(url: string, opts: RequestInit) => {
const body = JSON.parse(opts.body as string);
(callLog) => {
const body = JSON.parse(callLog.options.body as string);
resolve(body.messages);
return {};
},
+91 -129
View File
@@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { type Mocked } from "jest-mock";
import { type Mocked } from "vitest";
import {
createClient,
encodeBase64,
type IContent,
type ICreateClientOpts,
type IEvent,
type IMegolmSessionData,
@@ -69,10 +70,11 @@ function mockUploadEmitter(
expectedVersion: string,
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
const emitter = new TypedEventEmitter();
fetchMock.removeRoute("mock-upload-emitter");
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const version = new URLSearchParams(new URL(url).search).get("version");
(callLog) => {
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version != expectedVersion) {
return {
status: 403,
@@ -83,7 +85,7 @@ function mockUploadEmitter(
},
};
}
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) {
@@ -99,9 +101,7 @@ function mockUploadEmitter(
},
};
},
{
overwriteRoutes: true,
},
{ name: "mock-upload-emitter" },
);
return emitter;
}
@@ -117,12 +117,10 @@ describe("megolm-keys backup", () => {
let e2eKeyResponder: E2EKeyResponder;
beforeEach(async () => {
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
mockInitialApiRequests(TEST_HOMESERVER_URL);
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
@@ -133,15 +131,12 @@ describe("megolm-keys backup", () => {
});
afterEach(async () => {
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}
aliceClient?.stopClient();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
await vi.runAllTimersAsync();
fetchMock.mockReset();
jest.restoreAllMocks();
vi.restoreAllMocks();
});
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
@@ -207,9 +202,9 @@ describe("megolm-keys backup", () => {
);
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (callLog) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA;
} else {
@@ -237,11 +232,11 @@ describe("megolm-keys backup", () => {
// Eventually, decryption succeeds.
await awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
});
it("handles error on backup query gracefully", async () => {
jest.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
@@ -253,9 +248,9 @@ describe("megolm-keys backup", () => {
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest();
const calls = fetchMock.calls("getKey");
const calls = fetchMock.callHistory.calls("getKey");
expect(calls.length).toEqual(1);
expect(calls[0][0]).toEqual(EXPECTED_URL);
expect(calls[0].url).toEqual(EXPECTED_URL);
await flushBackupRequest();
@@ -274,11 +269,11 @@ describe("megolm-keys backup", () => {
// Send Alice a message that she won't be able to decrypt
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest();
const calls = fetchMock.calls("getKey");
const calls = fetchMock.callHistory.calls("getKey");
expect(calls.length).toEqual(1);
expect(calls[0][0]).toEqual(EXPECTED_URL);
expect(calls[0].url).toEqual(EXPECTED_URL);
fetchMock.resetHistory();
fetchMock.clearHistory();
// another message
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
@@ -288,7 +283,7 @@ describe("megolm-keys backup", () => {
};
syncResponder.sendOrQueueSyncResponse(syncResponse2);
await flushBackupRequest();
expect(fetchMock.calls("getKey").length).toEqual(0);
expect(fetchMock.callHistory.calls("getKey").length).toEqual(0);
});
});
@@ -364,9 +359,9 @@ describe("megolm-keys backup", () => {
}
it("Should import full backup in chunks", async function () {
const importMockImpl = jest.fn();
const importMockImpl = vi.fn();
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
// We need several rooms with several sessions to test chunking
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
@@ -380,7 +375,7 @@ describe("megolm-keys backup", () => {
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const progressCallback = vi.fn();
const result = await aliceCrypto.restoreKeyBackup({
progressCallback,
});
@@ -417,7 +412,7 @@ describe("megolm-keys backup", () => {
});
it("Should continue to process backup if a chunk import fails and report failures", async function () {
const importMockImpl = jest
const importMockImpl = vi
.fn()
.mockImplementationOnce(() => {
// Fail to import first chunk
@@ -427,7 +422,7 @@ describe("megolm-keys backup", () => {
.mockResolvedValue(undefined);
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
@@ -439,7 +434,7 @@ describe("megolm-keys backup", () => {
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const progressCallback = vi.fn();
const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
expect(result.total).toStrictEqual(expectedTotal);
@@ -463,13 +458,13 @@ describe("megolm-keys backup", () => {
it("Should continue if some keys fails to decrypt", async function () {
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = jest.fn();
aliceCrypto.importBackedUpRoomKeys = vi.fn();
const decryptionFailureCount = 2;
const mockDecryptor = {
// DecryptSessions does not reject on decryption failure, but just skip the key
decryptSessions: jest.fn().mockImplementation((sessions) => {
decryptSessions: vi.fn().mockImplementation((sessions) => {
// simulate fail to decrypt 2 keys out of all
const decrypted = [];
const keys = Object.keys(sessions);
@@ -480,11 +475,11 @@ describe("megolm-keys backup", () => {
}
return decrypted;
}),
free: jest.fn(),
free: vi.fn(),
};
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.getBackupDecryptor = jest.fn().mockResolvedValue(mockDecryptor);
aliceCrypto.getBackupDecryptor = vi.fn().mockResolvedValue(mockDecryptor);
const { response, expectedTotal } = createBackupDownloadResponse([100]);
@@ -505,7 +500,7 @@ describe("megolm-keys backup", () => {
it("Should get the decryption key from the secret storage and restore the key backup", async function () {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
const fullBackup = {
rooms: {
@@ -534,7 +529,7 @@ describe("megolm-keys backup", () => {
describe("backupLoop", () => {
it("Alice should upload known keys when backup is enabled", async function () {
// 404 means that there is no active backup
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404, { name: "room-keys-version" });
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -571,8 +566,8 @@ describe("megolm-keys backup", () => {
});
});
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
fetchMock.modifyRoute("room-keys-version", {
response: { status: 200, body: testData.SIGNED_BACKUP_DATA },
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
@@ -581,7 +576,7 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
vi.runAllTimers();
await Promise.all(uploadPromises);
@@ -605,7 +600,7 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
vi.runAllTimers();
await newKeyUploadPromise;
});
@@ -630,7 +625,7 @@ describe("megolm-keys backup", () => {
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
name: "room-keys-version",
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
@@ -640,7 +635,7 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
vi.runAllTimers();
// wait for all keys to be backed up
await remainingZeroPromise;
@@ -651,10 +646,7 @@ describe("megolm-keys backup", () => {
newBackup.version = newBackupVersion;
// Let's simulate that a new backup is available by returning error code on key upload
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
overwriteRoutes: true,
});
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
// If we import a new key the loop will try to upload to old version, it will
// fail then check the current version and switch if trusted
@@ -697,12 +689,12 @@ describe("megolm-keys backup", () => {
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
vi.runAllTimers();
await disableOldBackup;
await enableNewBackup;
jest.runAllTimers();
vi.runAllTimers();
await Promise.all(uploadPromises);
await newKeyUploadPromise;
@@ -717,22 +709,14 @@ describe("megolm-keys backup", () => {
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// on the first key upload attempt, simulate a network failure
const failurePromise = new Promise((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
() => {
resolve(undefined);
throw new TypeError(`Failed to fetch`);
},
{
overwriteRoutes: true,
},
);
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
resolve(undefined);
throw new TypeError(`Failed to fetch`);
});
});
// kick the import loop off and wait for the failed request
@@ -741,27 +725,21 @@ describe("megolm-keys backup", () => {
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
jest.advanceTimersByTime(10 * 60 * 1000);
vi.advanceTimersByTime(10 * 60 * 1000);
await failurePromise;
// Fix the endpoint to do successful uploads
const successPromise = new Promise((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
() => {
resolve(undefined);
return {
status: 200,
body: {
count: 2,
etag: "abcdefg",
},
};
},
{
overwriteRoutes: true,
},
);
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
resolve(undefined);
return {
status: 200,
body: {
count: 2,
etag: "abcdefg",
},
};
});
});
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
@@ -774,7 +752,7 @@ describe("megolm-keys backup", () => {
});
// run the timers, which will make the backup loop redo the request
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
await vi.advanceTimersByTimeAsync(10 * 60 * 1000);
await successPromise;
await allKeysUploadedPromise;
});
@@ -782,7 +760,7 @@ describe("megolm-keys backup", () => {
it("getActiveSessionBackupVersion() should give correct result", async function () {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -801,9 +779,7 @@ describe("megolm-keys backup", () => {
// Serve a backup with no trusted signature
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
delete unsignedBackup.auth_data.signatures;
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", unsignedBackup);
const checked = await aliceCrypto.checkKeyBackupAndEnable();
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
@@ -813,9 +789,7 @@ describe("megolm-keys backup", () => {
expect(backupStatus).toBeNull();
// Add a valid signature to the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// check that signalling is working
const backupPromise = new Promise<void>((resolve, reject) => {
@@ -837,7 +811,7 @@ describe("megolm-keys backup", () => {
it("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
aliceClient = await initTestClient();
@@ -853,11 +827,9 @@ describe("megolm-keys backup", () => {
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
// Return now the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
expect(await aliceCrypto.getKeyBackupInfo()).toMatchObject(testData.SIGNED_BACKUP_DATA);
// Delete the backup and we are expecting the key backup to be disabled
const keyBackupStatus = Promise.withResolvers<boolean>();
@@ -991,7 +963,7 @@ describe("megolm-keys backup", () => {
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
@@ -1001,9 +973,7 @@ describe("megolm-keys backup", () => {
delete unsignedBackup.auth_data.signatures;
unsignedBackup.version = "2";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", unsignedBackup);
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
@@ -1018,7 +988,7 @@ describe("megolm-keys backup", () => {
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
@@ -1028,9 +998,7 @@ describe("megolm-keys backup", () => {
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
newBackup.version = newBackupVersion;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
overwriteRoutes: true,
});
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", newBackup);
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
@@ -1045,25 +1013,19 @@ describe("megolm-keys backup", () => {
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
{
overwriteRoutes: true,
},
);
});
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
expect(noResult).toBeNull();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
@@ -1072,10 +1034,12 @@ describe("megolm-keys backup", () => {
describe("Backup Changed from other sessions", () => {
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
name: "room-keys-version",
});
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
fetchMock.getOnce("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
@@ -1108,9 +1072,9 @@ describe("megolm-keys backup", () => {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
(url, request) => {
(callLog) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA;
} else {
@@ -1124,7 +1088,7 @@ describe("megolm-keys backup", () => {
};
}
},
{ overwriteRoutes: true },
{ name: "room-keys" },
);
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
@@ -1135,7 +1099,7 @@ describe("megolm-keys backup", () => {
const event = room.getLiveTimeline().getEvents()[0];
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
// =====
// Second suppose now that the backup has changed to version 2
@@ -1146,7 +1110,7 @@ describe("megolm-keys backup", () => {
version: "2",
};
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, { overwriteRoutes: true });
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
// suppose the new key is now known
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
@@ -1159,11 +1123,10 @@ describe("megolm-keys backup", () => {
const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
(url, request) => {
fetchMock.modifyRoute("room-keys", {
response: (callLog) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
if (version == newBackup.version) {
awaitHasQueriedNewBackup.resolve();
return testData.CURVE25519_KEY_BACKUP_DATA;
@@ -1179,8 +1142,7 @@ describe("megolm-keys backup", () => {
};
}
},
{ overwriteRoutes: true },
);
});
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the new backup.
const newMessage: Partial<IEvent> = {
@@ -1216,7 +1178,7 @@ describe("megolm-keys backup", () => {
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
+20 -38
View File
@@ -16,7 +16,8 @@ limitations under the License.
import Olm from "@matrix-org/olm";
import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
import {
type IContent,
@@ -32,7 +33,6 @@ import { type ISyncResponder } from "../../test-utils/SyncResponder";
import { syncPromise } from "../../test-utils/test-utils";
import { type KeyBackupInfo } from "../../../src/crypto-api";
import { logger } from "../../../src/logger";
import type FetchMock from "fetch-mock";
/**
* @module
@@ -305,7 +305,9 @@ export function encryptMegolmEventRawPlainText(opts: {
},
type: "m.room.encrypted",
unsigned: {},
state_key: opts.plaintext.state_key ? `${opts.plaintext.type}:${opts.plaintext.state_key}` : undefined,
state_key: opts.plaintext.hasOwnProperty("state_key")
? `${opts.plaintext.type}:${opts.plaintext.state_key}`
: undefined,
};
}
@@ -460,19 +462,11 @@ export async function expectSendRoomKey(
return inboundGroupSession;
}
return await new Promise<Olm.InboundGroupSession>((resolve) => {
fetchMock.putOnce(
new RegExp("/sendToDevice/m.room.encrypted/"),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
resolve(onSendRoomKey(content));
return {};
},
{
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
overwriteRoutes: false,
},
);
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
const content = JSON.parse(callLog.options.body as string);
resolve(onSendRoomKey(content));
return {};
});
});
}
@@ -483,17 +477,11 @@ export async function expectSendRoomKey(
*/
export function expectEncryptedSendMessageEvent() {
return new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp("/send/m.room.encrypted/"),
(url, request) => {
const content = JSON.parse(request.body as string);
resolve(content);
return { event_id: "$event_id" };
},
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
{ overwriteRoutes: false },
);
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), (callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return { event_id: "$event_id" };
});
});
}
@@ -504,17 +492,11 @@ export function expectEncryptedSendMessageEvent() {
*/
function expectEncryptedSendStateEvent() {
return new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp("/state/m.room.encrypted/"),
(url, request) => {
const content = JSON.parse(request.body as string);
resolve(content);
return { event_id: "$event_id" };
},
// append to the list of intercepts on this path (since we have some tests that call
// this function multiple times)
{ overwriteRoutes: false },
);
fetchMock.putOnce(new RegExp("/state/m.room.encrypted/"), (callLog) => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return { event_id: "$event_id" };
});
});
}
+6 -8
View File
@@ -16,7 +16,7 @@ limitations under the License.
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { createClient, IndexedDBCryptoStore } from "../../../src";
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
@@ -26,7 +26,7 @@ import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostor
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
import { CryptoEvent } from "../../../src/crypto-api";
jest.setTimeout(15000);
vi.setConfig({ testTimeout: 15000 });
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -122,6 +122,7 @@ describe("MatrixClient.initRustCrypto", () => {
);
});
// eslint-disable-next-line @vitest/expect-expect
it("should ignore a second call", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -134,10 +135,6 @@ describe("MatrixClient.initRustCrypto", () => {
});
describe("Libolm Migration", () => {
beforeEach(() => {
fetchMock.reset();
});
it("should migrate from libolm", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
@@ -155,7 +152,7 @@ describe("MatrixClient.initRustCrypto", () => {
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
});
const progressListener = jest.fn();
const progressListener = vi.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
@@ -326,7 +323,7 @@ describe("MatrixClient.initRustCrypto", () => {
});
// When we start Rust crypto, potentially triggering an upgrade
const progressListener = jest.fn();
const progressListener = vi.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
@@ -478,6 +475,7 @@ describe("MatrixClient.clearStores", () => {
expect(await indexedDB.databases()).toHaveLength(0);
});
// eslint-disable-next-line @vitest/expect-expect
it("should not fail in environments without indexedDB", async () => {
// eslint-disable-next-line no-global-assign
indexedDB = undefined!;
+4 -8
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import Olm from "@matrix-org/olm";
@@ -72,7 +72,6 @@ describe("Encrypted State Events", () => {
beforeEach(async () => {
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
@@ -96,15 +95,11 @@ describe("Encrypted State Events", () => {
}, 10000);
afterEach(async () => {
await aliceClient.stopClient();
await jest.runAllTimersAsync();
fetchMock.mockReset();
aliceClient.stopClient();
});
function expectAliceKeyQuery(response: any) {
fetchMock.postOnce(new RegExp("/keys/query"), (url: string, opts: RequestInit) => response, {
overwriteRoutes: false,
});
fetchMock.postOnce(new RegExp("/keys/query"), (callLog) => response);
}
function expectAliceKeyClaim(response: any) {
@@ -190,6 +185,7 @@ describe("Encrypted State Events", () => {
expect(decryptedEvent.getContent().topic).toEqual("Secret!");
});
// eslint-disable-next-line @vitest/expect-expect
it("Should send an encrypted state event", async () => {
const homeserverUrl = aliceClient.getHomeserverUrl();
const keyResponder = new E2EKeyResponder(homeserverUrl);
+1 -3
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import Olm from "@matrix-org/olm";
@@ -59,7 +59,6 @@ describe("to-device-messages", () => {
async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://server.com";
aliceClient = createClient({
@@ -100,7 +99,6 @@ describe("to-device-messages", () => {
afterEach(async () => {
aliceClient.stopClient();
fetchMock.mockReset();
});
describe("encryptToDeviceMessages", () => {
+71 -92
View File
@@ -18,12 +18,12 @@ import "fake-indexeddb/auto";
import anotherjson from "another-json";
import debug from "debug";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto";
import Olm from "@matrix-org/olm";
import type FetchMock from "fetch-mock";
import {
createClient,
DebugLogger,
@@ -92,10 +92,7 @@ beforeAll(async () => {
}, 10000);
beforeEach(() => {
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
// to ensure that we don't end up with dangling timeouts.
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
});
afterEach(() => {
@@ -130,7 +127,6 @@ describe("verification", () => {
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
@@ -141,14 +137,12 @@ describe("verification", () => {
});
afterEach(async () => {
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}
aliceClient?.stopClient();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
if (vi.isFakeTimers()) {
await vi.runAllTimersAsync();
}
});
describe("Outgoing verification requests for another device", () => {
@@ -156,11 +150,10 @@ describe("verification", () => {
// pretend that we have another device, which we will verify
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`),
{ ok: false, status: 404 },
{ overwriteRoutes: true },
);
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`), {
ok: false,
status: 404,
});
});
// test with (1) the default verification method list, (2) a custom verification method list.
@@ -212,7 +205,7 @@ describe("verification", () => {
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
if (methods !== undefined) {
// eslint-disable-next-line jest/no-conditional-expect
// eslint-disable-next-line @vitest/no-conditional-expect
expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
}
@@ -245,7 +238,7 @@ describe("verification", () => {
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
vi.advanceTimersByTime(10);
requestBody = await sendToDevicePromise;
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
@@ -323,7 +316,7 @@ describe("verification", () => {
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// And now Alice starts a SAS verification
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
@@ -516,7 +509,7 @@ describe("verification", () => {
// Rust crypto waits for the 'done' to arrive from the other side.
if (request.phase === VerificationPhase.Done) {
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
// eslint-disable-next-line jest/no-conditional-expect
// eslint-disable-next-line @vitest/no-conditional-expect
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
await verificationPromise;
}
@@ -639,7 +632,7 @@ describe("verification", () => {
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// ... but Alice wants to do an SAS verification
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
@@ -684,7 +677,7 @@ describe("verification", () => {
expect(request.verifier).toBeUndefined();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// ... but the dummy device wants to do an SAS verification
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
@@ -792,7 +785,7 @@ describe("verification", () => {
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
vi.advanceTimersByTime(10);
await sendToDevicePromise;
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
@@ -937,19 +930,16 @@ describe("verification", () => {
function awaitRoomMessageRequest(): Promise<IContent> {
return new Promise((resolve) => {
// Case of unencrypted message of the new crypto
fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
(url: string, options: RequestInit) => {
resolve(JSON.parse(options.body as string));
return { event_id: "$YUwRidLecu:example.com" };
},
);
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", (callLog) => {
resolve(JSON.parse(callLog.options.body as string));
return { event_id: "$YUwRidLecu:example.com" };
});
// Case of encrypted message of the old crypto
fetchMock.put(
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
async (url: string, options: RequestInit) => {
const encryptedMessage = JSON.parse(options.body as string);
async (callLog) => {
const encryptedMessage = JSON.parse(callLog.options.body as string);
const event = new MatrixEvent({
content: encryptedMessage,
type: "m.room.encrypted",
@@ -972,7 +962,7 @@ describe("verification", () => {
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
// 5ms by users, ie Bob and Alice
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
const messageRequestPromise = awaitRoomMessageRequest();
const verificationRequest = await aliceClient
@@ -1082,14 +1072,14 @@ describe("verification", () => {
});
it("ignores old verification requests", async () => {
const debug = jest.fn();
const info = jest.fn();
const warn = jest.fn();
const debug = vi.fn();
const info = vi.fn();
const warn = vi.fn();
// @ts-ignore overriding RustCrypto's logger
aliceClient.getCrypto()!.logger = { debug, info, warn };
const eventHandler = jest.fn();
const eventHandler = vi.fn();
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
const verificationRequestEvent = createVerificationRequestEvent();
@@ -1105,7 +1095,7 @@ describe("verification", () => {
// Wait until the request has been processed. We use a real sleep()
// here to make sure any background async tasks are completed.
jest.useRealTimers();
vi.useRealTimers();
await waitFor(async () => {
expect(info).toHaveBeenCalledWith(
expect.stringMatching(/^Ignoring just-received verification request/),
@@ -1187,7 +1177,7 @@ describe("verification", () => {
returnToDeviceMessageFromSync(toDeviceEvent);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// Wait for the request to be decrypted
const request1 = await requestEventPromise;
@@ -1224,7 +1214,7 @@ describe("verification", () => {
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// Advance time by 5mins, the verification request should be ignored after that
jest.advanceTimersByTime(5 * 60 * 1000);
vi.advanceTimersByTime(5 * 60 * 1000);
// Send Bob the room keys
returnToDeviceMessageFromSync(toDeviceEvent);
@@ -1290,7 +1280,7 @@ describe("verification", () => {
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
await syncPromise(aliceClient);
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// The client should now know about the olm device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
@@ -1302,11 +1292,10 @@ describe("verification", () => {
testOlmAccount?.free();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
await vi.runAllTimersAsync();
});
// eslint-disable-next-line @vitest/expect-expect
it("Should request cross signing keys after verification", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
@@ -1424,11 +1413,11 @@ describe("verification", () => {
*/
async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> {
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
vi.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
vi.useFakeTimers();
return aliceClient.getCrypto()!.getSessionBackupPrivateKey();
}
@@ -1461,27 +1450,21 @@ describe("verification", () => {
});
const expectBackupCheck = new Promise((resolve) => {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/version",
(url, request) => {
resolve(undefined);
if (expectBackup instanceof MatrixError) {
return {
status: expectBackup.httpStatus,
body: expectBackup.data,
};
}
fetchMock.get("express:/_matrix/client/v3/room_keys/version", (callLog) => {
resolve(undefined);
if (expectBackup instanceof MatrixError) {
return {
status: expectBackup.httpStatus,
body: expectBackup.data,
};
}
if (expectBackup instanceof Error) {
return Promise.reject(expectBackup);
}
if (expectBackup instanceof Error) {
return Promise.reject(expectBackup);
}
return expectBackup;
},
{
overwriteRoutes: true,
},
);
return expectBackup;
});
});
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
@@ -1562,7 +1545,7 @@ describe("verification", () => {
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
await vi.advanceTimersByTimeAsync(10);
// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
@@ -1596,8 +1579,8 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
return new Promise((resolve) => {
fetchMock.putOnce(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
resolve(JSON.parse(opts.body as string));
(callLog): RouteResponse => {
resolve(JSON.parse(callLog.options.body as string));
return {};
},
);
@@ -1618,29 +1601,25 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
const uskRequestResolvers = Promise.withResolvers<string>();
const backupKeyRequestResolvers = Promise.withResolvers<string>();
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const content = Object.values(messages)[0] as any;
if (content.action == "request") {
const name = content.name;
const requestId = content.request_id;
if (name == "m.cross_signing.user_signing") {
uskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.master") {
mskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.self_signing") {
sskRequestResolvers.resolve(requestId);
} else if (name == "m.megolm_backup.v1") {
backupKeyRequestResolvers.resolve(requestId);
}
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`), (callLog): RouteResponse => {
const messages = JSON.parse(callLog.options.body as string).messages[TEST_USER_ID];
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const content = Object.values(messages)[0] as any;
if (content.action == "request") {
const name = content.name;
const requestId = content.request_id;
if (name == "m.cross_signing.user_signing") {
uskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.master") {
mskRequestResolvers.resolve(requestId);
} else if (name == "m.cross_signing.self_signing") {
sskRequestResolvers.resolve(requestId);
} else if (name == "m.megolm_backup.v1") {
backupKeyRequestResolvers.resolve(requestId);
}
return {};
},
{ overwriteRoutes: true },
);
}
return {};
});
const promiseMap = new Map<string, Promise<string>>();
promiseMap.set("m.cross_signing.master", mskRequestResolvers.promise);
@@ -672,7 +672,7 @@ describe("MatrixClient event timelines", function () {
expect(timeline!.getEvents().find((e) => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
});
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
it("should return null when event is not in the thread that the given timelineSet is representing", () => {
// @ts-ignore
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
});
return Promise.all([
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeUndefined(),
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeNull(),
httpBackend.flushAllExpected(),
]);
});
it("should return undefined when event is within a thread but timelineSet is not", () => {
it("should return null when event is within a thread but timelineSet is not", () => {
// @ts-ignore
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
});
return Promise.all([
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeUndefined(),
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeNull(),
httpBackend.flushAllExpected(),
]);
});
@@ -2044,6 +2044,7 @@ describe("MatrixClient event timelines", function () {
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
}
// eslint-disable-next-line @vitest/expect-expect
it("in stable mode", async () => {
// @ts-ignore
client.clientOpts.threadSupport = true;
+10 -13
View File
@@ -347,6 +347,7 @@ describe("MatrixClient", function () {
expect((await prom).room_id).toBe(roomId);
});
// eslint-disable-next-line @vitest/expect-expect
it("should no-op if you've already knocked a room", function () {
const room = new Room(roomId, client, userId);
@@ -380,23 +381,16 @@ describe("MatrixClient", function () {
[
403,
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
"[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]",
],
[
500,
{ errcode: "INTERNAL_SERVER_ERROR" },
"[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]",
"MatrixError: [403] You don't have permission to knock",
],
[500, { errcode: "INTERNAL_SERVER_ERROR" }, "MatrixError: [500] Unknown message"],
];
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
const prom = client.knockRoom(roomId);
await Promise.all([
httpBackend.flushAllExpected(),
expect(prom).rejects.toMatchInlineSnapshot(snapshot),
]);
await Promise.all([httpBackend.flushAllExpected(), expect(prom).rejects.toThrow(snapshot)]);
});
});
});
@@ -1198,7 +1192,7 @@ describe("MatrixClient", function () {
describe("logout", () => {
it("should abort pending requests when called with stopClient=true", async () => {
httpBackend.when("POST", "/logout").respond(200, {});
const fn = jest.fn();
const fn = vi.fn();
client.http.request(Method.Get, "/test").catch(fn);
client.logout(true);
await httpBackend.flush(undefined);
@@ -1326,7 +1320,7 @@ describe("MatrixClient", function () {
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});
it("should always fetch capabilities and then cache", async () => {
@@ -1397,6 +1391,7 @@ describe("MatrixClient", function () {
});
describe("publicRooms", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should use GET request if no server or filter is specified", () => {
httpBackend.when("GET", "/publicRooms").respond(200, {});
client.publicRooms({});
@@ -1585,7 +1580,7 @@ describe("MatrixClient", function () {
describe("setSyncPresence", () => {
it("should pass calls through to the underlying sync api", () => {
const setPresence = jest.fn();
const setPresence = vi.fn();
// @ts-ignore
client.syncApi = { setPresence };
client.setSyncPresence(SetPresence.Unavailable);
@@ -1594,6 +1589,7 @@ describe("MatrixClient", function () {
});
describe("sendTyping", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should bail early for guests", async () => {
client.setGuest(true);
await client.sendTyping("!room:server", true, 100);
@@ -1851,6 +1847,7 @@ describe("MatrixClient", function () {
});
describe("setRoomMutePushRule", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should set room push rule to muted", async () => {
const roomId = "!roomId:server";
const client = new MatrixClient({
+1 -1
View File
@@ -159,7 +159,7 @@ describe("MatrixClient opts", function () {
await expect(
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
).rejects.toThrow("MatrixError: [500] Unknown message");
).rejects.toThrow("MatrixError: [500] Ruh roh");
});
it("shouldn't queue events", async () => {
@@ -720,7 +720,7 @@ describe("MatrixClient room timelines", function () {
} else {
reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire."));
}
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Vitest? */);
room.on(RoomEvent.TimelineReset, async () => {
try {
@@ -15,7 +15,7 @@ limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
@@ -83,8 +83,7 @@ describe("MatrixClient syncing errors", () => {
});
it("should retry, until errors are solved.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
vi.useFakeTimers();
fetchMock
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
@@ -105,19 +104,18 @@ describe("MatrixClient syncing errors", () => {
await client!.startClient();
expect(await syncEvents[0].promise).toBe(SyncState.Error);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[1].promise).toBe(SyncState.Error);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
});
it("should stop sync keep alive when client is stopped.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
vi.useFakeTimers();
fetchMock
.get("end:capabilities", {})
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
@@ -146,9 +144,9 @@ describe("MatrixClient syncing errors", () => {
const syntState = await firstSyncEvent.promise;
expect(syntState).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive
vi.runAllTimers(); // this will skip forward to trigger the keepAlive
jest.useRealTimers(); // we need real timer for the setTimout below to work
vi.useRealTimers(); // we need real timer for the setTimout below to work
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
+5 -4
View File
@@ -94,6 +94,7 @@ describe("MatrixClient syncing", () => {
presence: {},
};
// eslint-disable-next-line @vitest/expect-expect
it("should /sync after /pushrules and /filter.", async () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
@@ -501,7 +502,7 @@ describe("MatrixClient syncing", () => {
})
.respond(200, syncData);
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
client!.startClient({ initialSyncLimit: 1 });
return httpBackend!.flushAllExpected();
@@ -994,7 +995,7 @@ describe("MatrixClient syncing", () => {
roomVersion: "org.matrix.msc2716v3",
},
].forEach((testMeta) => {
// eslint-disable-next-line jest/valid-title
// eslint-disable-next-line @vitest/valid-title
describe(testMeta.label, () => {
const roomCreateEvent = utils.mkEvent({
type: "m.room.create",
@@ -1835,7 +1836,7 @@ describe("MatrixClient syncing", () => {
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
room!.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
room!.hasEncryptionStateEvent = vi.fn().mockReturnValue(true);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
@@ -2519,7 +2520,7 @@ describe("MatrixClient syncing", () => {
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
client!.store.storeAccountDataEvents([eventA1, eventB1]);
const fn = jest.fn();
const fn = vi.fn();
client!.on(ClientEvent.AccountData, fn);
httpBackend!.when("GET", "/sync").respond(200, {
@@ -62,7 +62,7 @@ describe("Notification count fixing", () => {
client!.startClient({ threadSupport: true });
const room = new Room(roomId, client!, selfUserId);
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
const event = new MatrixEvent({
room_id: roomId,
@@ -77,7 +77,7 @@ describe("Notification count fixing", () => {
},
});
jest.spyOn(event, "getPushActions").mockReturnValue({
vi.spyOn(event, "getPushActions").mockReturnValue({
notify: true,
tweaks: {},
});
@@ -123,7 +123,7 @@ describe("MatrixClient syncing", () => {
]);
const room = new Room(roomId, client!, selfUserId);
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!;
@@ -143,7 +143,7 @@ describe("MatrixClient syncing", () => {
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
let lastEvent: MatrixEvent | null = null;
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
vi.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
lastEvent = event as MatrixEvent;
return { event_id: reactionEventId };
});
@@ -195,7 +195,7 @@ describe("MatrixClient syncing", () => {
})
.respond(200, syncData);
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
client!.startClient({ initialSyncLimit: 1 });
await httpBackend!.flushAllExpected();
@@ -15,8 +15,7 @@ limitations under the License.
*/
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
import { mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import {
MSC4108FailureReason,
@@ -40,7 +39,7 @@ import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
const baseUrl = "https://example.com";
const crypto = {
exportSecretsForQrLogin: jest.fn(),
exportSecretsForQrLogin: vi.fn(),
};
const client = {
doesServerSupportUnstableFeature(feature: string) {
@@ -54,9 +53,9 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
},
baseUrl,
getDomain: () => "example.com",
getDevice: jest.fn(),
getCrypto: jest.fn(() => crypto),
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
getDevice: vi.fn(),
getCrypto: vi.fn(() => crypto),
getAuthMetadata: vi.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
} as unknown as MatrixClient;
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
baseUrl: client.baseUrl,
@@ -77,10 +76,6 @@ describe("MSC4108SignInWithQR", () => {
});
});
afterEach(() => {
fetchMock.reset();
});
const url = "https://fallbackserver/rz/123";
const deviceId = "DEADB33F";
const verificationUri = "https://example.com/verify";
@@ -115,10 +110,10 @@ describe("MSC4108SignInWithQR", () => {
let opponentData = Promise.withResolvers<string>();
const ourMockSession = {
send: jest.fn(async (newData) => {
send: vi.fn(async (newData) => {
ourData.resolve(newData);
}),
receive: jest.fn(() => {
receive: vi.fn(() => {
const prom = opponentData.promise;
prom.then(() => {
opponentData = Promise.withResolvers();
@@ -134,10 +129,10 @@ describe("MSC4108SignInWithQR", () => {
},
} as unknown as MSC4108RendezvousSession;
const opponentMockSession = {
send: jest.fn(async (newData) => {
send: vi.fn(async (newData) => {
opponentData.resolve(newData);
}),
receive: jest.fn(() => {
receive: vi.fn(() => {
const prom = ourData.promise;
prom.then(() => {
ourData = Promise.withResolvers();
@@ -171,7 +166,7 @@ describe("MSC4108SignInWithQR", () => {
it("should be able to connect with opponent and share verificationUri", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
@@ -194,7 +189,7 @@ describe("MSC4108SignInWithQR", () => {
it("should abort if device already exists", async () => {
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
await Promise.all([
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
@@ -244,12 +239,12 @@ describe("MSC4108SignInWithQR", () => {
// @ts-ignore
await opponentLogin.receive();
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
};
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
const payload = {
secrets: expect.objectContaining(secrets),
@@ -261,13 +256,13 @@ describe("MSC4108SignInWithQR", () => {
});
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
});
jest.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
vi.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + vi.mocked(setTimeout).mock.calls.length * 1000;
});
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
@@ -280,7 +275,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.send({
type: PayloadType.Success,
});
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
const ourProm = ourLogin.shareSecrets();
await expect(ourProm).rejects.toThrow("New device not found");
@@ -297,7 +292,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.send({
type: PayloadType.Success,
});
mocked(client.getDevice).mockRejectedValue(
vi.mocked(client.getDevice).mockRejectedValue(
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
);
@@ -314,7 +309,7 @@ describe("MSC4108SignInWithQR", () => {
});
it("should not send secrets if user cancels", async () => {
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
@@ -334,7 +329,7 @@ describe("MSC4108SignInWithQR", () => {
await opponentLogin.receive();
const deviceResolvers = Promise.withResolvers<IMyDevice>();
mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
vi.mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
deviceResolvers.resolve({} as IMyDevice);
@@ -342,7 +337,7 @@ describe("MSC4108SignInWithQR", () => {
const secrets = {
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
};
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
await Promise.all([
expect(ourProm).rejects.toThrow("User cancelled"),
+21 -19
View File
@@ -26,7 +26,7 @@ import {
type Extension,
} from "../../src/sliding-sync";
import { TestClient } from "../TestClient";
import { type IRoomEvent, type IStateEvent } from "../../src";
import { type IContent, type IRoomEvent, type IStateEvent } from "../../src";
import {
type MatrixClient,
type MatrixEvent,
@@ -68,17 +68,17 @@ describe("SlidingSyncSdk", () => {
const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getListParams = jest.fn();
s.getListData = jest.fn();
s.getRoomSubscriptions = jest.fn();
s.modifyRoomSubscriptionInfo = jest.fn();
s.modifyRoomSubscriptions = jest.fn();
s.registerExtension = jest.fn();
s.setList = jest.fn();
s.setListRanges = jest.fn();
s.start = jest.fn();
s.stop = jest.fn();
s.resend = jest.fn();
s.getListParams = vi.fn();
s.getListData = vi.fn();
s.getRoomSubscriptions = vi.fn();
s.modifyRoomSubscriptionInfo = vi.fn();
s.modifyRoomSubscriptions = vi.fn();
s.registerExtension = vi.fn();
s.setList = vi.fn();
s.setListRanges = vi.fn();
s.start = vi.fn();
s.stop = vi.fn();
s.resend = vi.fn();
return s;
};
@@ -111,7 +111,7 @@ describe("SlidingSyncSdk", () => {
expect(m.getType()).toEqual(want[i].type);
expect(m.getSender()).toEqual(want[i].sender);
expect(m.getId()).toEqual(want[i].event_id);
expect(m.getContent()).toEqual(want[i].content);
expect(m.getContent<IContent>()).toEqual(want[i].content);
expect(m.getTs()).toEqual(want[i].origin_server_ts);
if (want[i].unsigned) {
expect(m.getUnsigned()).toEqual(want[i].unsigned);
@@ -150,7 +150,7 @@ describe("SlidingSyncSdk", () => {
// find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension<any, any> => {
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
const mockFn = vi.mocked(mockSlidingSync!.registerExtension);
// find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
@@ -658,7 +658,7 @@ describe("SlidingSyncSdk", () => {
});
it("can update device lists", () => {
syncCryptoCallback!.processDeviceLists = jest.fn();
syncCryptoCallback!.processDeviceLists = vi.fn();
ext.onResponse({
device_lists: {
changed: ["@alice:localhost"],
@@ -672,7 +672,7 @@ describe("SlidingSyncSdk", () => {
});
it("can update OTK counts and unused fallback keys", () => {
syncCryptoCallback!.processKeyCounts = jest.fn();
syncCryptoCallback!.processKeyCounts = vi.fn();
ext.onResponse({
device_one_time_keys_count: {
signed_curve25519: 42,
@@ -722,7 +722,7 @@ describe("SlidingSyncSdk", () => {
});
globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeTruthy();
expect(globalData.getContent()).toEqual(globalContent);
expect(globalData.getContent<IContent>()).toEqual(globalContent);
});
it("processes rooms account data", async () => {
@@ -757,7 +757,7 @@ describe("SlidingSyncSdk", () => {
expect(room).toBeTruthy();
const event = room.getAccountData(roomType)!;
expect(event).toBeTruthy();
expect(event.getContent()).toEqual(roomContent);
expect(event.getContent<IContent>()).toEqual(roomContent);
});
it("doesn't crash for unknown room account data", async () => {
@@ -847,6 +847,7 @@ describe("SlidingSyncSdk", () => {
});
});
// eslint-disable-next-line @vitest/expect-expect
it("can handle missing fields", async () => {
ext.onResponse({
next_batch: "23456",
@@ -861,7 +862,7 @@ describe("SlidingSyncSdk", () => {
};
let called = false;
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
expect(ev.getContent()).toEqual(toDeviceContent);
expect(ev.getContent<IContent>()).toEqual(toDeviceContent);
expect(ev.getType()).toEqual(toDeviceType);
called = true;
});
@@ -1095,6 +1096,7 @@ describe("SlidingSyncSdk", () => {
expect(receipt?.data.thread_id).toBeFalsy();
});
// eslint-disable-next-line @vitest/expect-expect
it("gracefully handles missing rooms when receiving receipts", async () => {
const roomId = "!room:id";
const alice = "@alice:alice";
+1
View File
@@ -82,6 +82,7 @@ describe("SlidingSync", () => {
await p;
});
// eslint-disable-next-line @vitest/expect-expect
it("should stop the sync loop upon calling stop()", () => {
slidingSync.stop();
httpBackend!.verifyNoOutstandingExpectation();
+13 -4
View File
@@ -14,13 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
jest.mock("../src/http-api/utils", () => ({
...jest.requireActual("../src/http-api/utils"),
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
vi.mock("../src/http-api/utils", async () => ({
...(await vi.importActual("../src/http-api/utils")),
// We mock timeoutSignal otherwise it causes tests to leave timers running
timeoutSignal: () => new AbortController().signal,
}));
// Dont make test fail too soon due to timeouts while debugging.
manageFetchMockGlobally();
beforeEach(() => {
fetchMock.hardReset();
fetchMock.mockGlobal();
});
// Don't make test fail too soon due to timeouts while debugging.
if (process.env.VSCODE_INSPECTOR_OPTIONS) {
jest.setTimeout(60 * 1000 * 5); // 5 minutes
vi.setConfig({ testTimeout: 60 * 1000 * 5 }); // 5 minutes
}
+60 -45
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type ISyncResponder } from "./SyncResponder";
@@ -36,65 +36,80 @@ export class AccountDataAccumulator {
public constructor(private syncResponder: ISyncResponder) {}
private accountDataResolvers = new Map<string, PromiseWithResolvers<any>>();
private setInterceptRunning = false;
/**
* Intercept requests to set a particular type of account data.
* Intercept setting of account data.
*
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
* resolved.
*
* @param accountDataType - type of account data to be intercepted
* @param opts - options to pass to fetchMock
* @returns a Promise which will resolve (with the content of the account data) once it is set.
*/
public interceptSetAccountData(
accountDataType: string,
opts?: Parameters<(typeof fetchMock)["put"]>[2],
): Promise<any> {
return new Promise((resolve) => {
// Called when the cross signing key is uploaded
fetchMock.put(
`express:/_matrix/client/v3/user/:userId/account_data/${accountDataType}`,
(url: string, options: RequestInit) => {
const content = JSON.parse(options.body as string);
const type = url.split("/").pop();
// update account data for sync response
this.accountDataEvents.set(type!, content);
resolve(content);
public interceptSetAccountData(): void {
if (this.setInterceptRunning) return;
this.setInterceptRunning = true;
// return a sync response
this.sendSyncResponseWithUpdatedAccountData();
return {};
},
opts,
);
fetchMock.put(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
const content = JSON.parse(callLog.options.body as string);
const type = callLog.url.split("/").pop();
// update account data for sync response
this.accountDataEvents.set(type!, content);
this.accountDataResolvers.get(type!)?.resolve(content);
if (!this.accountDataResolvers.delete(type!)) {
// Check for a wildcard matcher
for (const [key, resolver] of this.accountDataResolvers) {
if (key.endsWith("*") && type?.startsWith(key.slice(0, -1))) {
resolver.resolve(content);
this.accountDataResolvers.delete(key);
}
}
}
// return a sync response
this.sendSyncResponseWithUpdatedAccountData();
return {};
});
}
/**
* Wait for a particular type of account data.
*
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
* resolved.
*
* @returns a Promise which will resolve (with the content of the account data) once it is set.
*/
public waitForAccountData(type: string): Promise<any> {
const resolvers = Promise.withResolvers<any>();
this.accountDataResolvers.set(type, resolvers);
this.interceptSetAccountData();
return resolvers.promise;
}
/**
* Intercept all requests to get account data
*/
public interceptGetAccountData(): void {
fetchMock.get(
`express:/_matrix/client/v3/user/:userId/account_data/:type`,
(url) => {
const type = url.split("/").pop();
const existing = this.accountDataEvents.get(type!);
if (existing) {
// return it
return {
status: 200,
body: existing,
};
} else {
// 404
return {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
};
}
},
{ overwriteRoutes: true },
);
fetchMock.get(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
const type = callLog.url.split("/").pop();
const existing = this.accountDataEvents.get(type!);
if (existing) {
// return it
return {
status: 200,
body: existing,
};
} else {
// 404
return {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
};
}
});
}
/**
+10 -9
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import debugFunc, { type Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
import type { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src";
@@ -81,26 +81,27 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
// set up a listener for /keys/upload.
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
const listener = (url: string, options: RequestInit) =>
this.onKeyUploadRequest(resolveOneTimeKeys, options);
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
fetchMock.post(
new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(),
(callLog) => this.onKeyUploadRequest(resolveOneTimeKeys, callLog.options),
{ name: routeNamePrefix + "keys-upload" },
);
});
fetchMock.post(
new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
(callLog) => this.onSignaturesUploadRequest(callLog.options),
{
url: new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
name: routeNamePrefix + "upload-sigs",
},
(url, options) => this.onSignaturesUploadRequest(options),
);
fetchMock.post(
new URL("/_matrix/client/v3/keys/device_signing/upload", homeserverUrl).toString(),
(callLog) => this.onSigningKeyUploadRequest(callLog.options),
{
url: new URL("/_matrix/client/v3/keys/device_signing/upload", homeserverUrl).toString(),
name: routeNamePrefix + "upload-cross-signing-keys",
},
(url, options) => this.onSigningKeyUploadRequest(options),
);
}
+4 -3
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { MapWithDefault } from "../../src/utils";
import { type IDownloadKeyResult, type SigningKeys } from "../../src";
@@ -42,8 +42,9 @@ export class E2EKeyResponder {
*/
public constructor(homeserverUrl: string) {
// set up a listener for /keys/query.
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), (callLog) =>
this.onKeyQueryRequest(callLog.options),
);
}
private onKeyQueryRequest(options: RequestInit) {
+4 -3
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { MapWithDefault } from "../../src/utils";
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
@@ -36,8 +36,9 @@ export class E2EOTKClaimResponder {
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
const listener = (url: string, options: RequestInit) => this.onKeyClaimRequest(options);
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), listener);
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), (callLog) =>
this.onKeyClaimRequest(callLog.options),
);
}
private onKeyClaimRequest(options: RequestInit) {
+4 -5
View File
@@ -16,9 +16,8 @@ limitations under the License.
import debugFunc from "debug";
import { type Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import type FetchMock from "fetch-mock";
import fetchMock from "@fetch-mock/vitest";
import { type RouteResponse } from "fetch-mock";
/** Interface implemented by classes that intercept `/sync` requests from test clients
*
@@ -76,12 +75,12 @@ export class SyncResponder implements ISyncResponder {
*/
public constructor(homeserverUrl: string) {
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (callLog) =>
this.onSyncRequest(),
);
}
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
private async onSyncRequest(): Promise<RouteResponse> {
switch (this.state) {
case SyncResponderState.IDLE: {
this.debug("Got /sync request: waiting for response to be ready");
+23 -17
View File
@@ -14,12 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MethodLikeKeys, mocked, type MockedObject } from "jest-mock";
import { type MockedObject } from "vitest";
import { type ClientEventHandlerMap, type EmittedEvents, type MatrixClient } from "../../src/client";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { User } from "../../src/models/user";
// Cribbed from https://github.com/jestjs/jest/blob/94830794dc5dfca1b49bc435b7b031b27838a798/packages/jest-mock/src/index.ts
type FunctionLike = (...args: any) => any;
type MethodLikeKeys<T> = keyof {
[K in keyof T as Required<T>[K] extends FunctionLike ? K : never]: T[K];
};
/**
* Mock client with real event emitter
* useful for testing code that listens
@@ -34,19 +40,19 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
/**
* - make a mock client
* - cast the type to mocked(MatrixClient)
* - cast the type to vi.mocked(MatrixClient)
* - spy on MatrixClientPeg.get to return the mock
* eg
* ```
* const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId),
getUserId: vi.fn().mockReturnValue(aliceId),
});
* ```
*/
export const getMockClientWithEventEmitter = (
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
): MockedObject<MatrixClient> => {
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
const mock = vi.mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
return mock;
};
@@ -59,14 +65,14 @@ export const getMockClientWithEventEmitter = (
* ```
*/
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
getUserId: jest.fn().mockReturnValue(userId),
getSafeUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
getUserId: vi.fn().mockReturnValue(userId),
getSafeUserId: vi.fn().mockReturnValue(userId),
getUser: vi.fn().mockReturnValue(new User(userId)),
isGuest: vi.fn().mockReturnValue(false),
mxcUrlToHttp: vi.fn().mockReturnValue("mock-mxcUrlToHttp"),
credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
getThreePids: vi.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: vi.fn(),
});
/**
@@ -78,16 +84,16 @@ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
* ```
*/
export const mockClientMethodsEvents = () => ({
decryptEventIfNeeded: jest.fn(),
getPushActionsForEvent: jest.fn(),
decryptEventIfNeeded: vi.fn(),
getPushActionsForEvent: vi.fn(),
});
/**
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCachedCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
getIdentityServerUrl: vi.fn(),
getHomeserverUrl: vi.fn(),
getCachedCapabilities: vi.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false),
});
+4 -2
View File
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MockInstance } from "vitest";
/**
* Filter emitter.emit mock calls to find relevant events
* eg:
* ```
* const emitSpy = jest.spyOn(state, 'emit');
* const emitSpy = vi.spyOn(state, 'emit');
* << actions >>
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
* expect(beaconLivenessEmits.length).toBe(1);
* ```
*/
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
export const filterEmitCallsByEventType = (eventType: string, spy: MockInstance<(...args: any[]) => any>) =>
spy.mock.calls.filter((args) => args[0] === eventType);
+4 -6
View File
@@ -14,12 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
// Vitest lacks tickAsync() and a number of other async methods which break the event loop,
// letting scheduled promise callbacks run. So we have to do it manually
// (this is what sinon does under the hood). We do both in a loop until the thing we expect happens:
// hopefully this is the least flakey way and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
export function flushPromises() {
return new Promise((r) => {
+31 -30
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { type KeyBackupInfo } from "../../src/crypto-api";
@@ -25,20 +25,15 @@ import { type KeyBackupInfo } from "../../src/crypto-api";
* @param userId - the local user's ID. Defaults to `@alice:localhost`.
*/
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
fetchMock.getOnce(
new URL("/_matrix/client/versions", homeserverUrl).toString(),
{ versions: ["v1.1"] },
{ overwriteRoutes: true },
);
fetchMock.getOnce(
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
{},
{ overwriteRoutes: true },
);
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
fetchMock.postOnce(
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(),
{ filter_id: "fid" },
{ overwriteRoutes: true },
);
fetchMock.getOnce(
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter/fid`, homeserverUrl).toString(),
{ filter_id: "fid" },
);
}
@@ -65,24 +60,30 @@ export function mockSetupCrossSigningRequests(): void {
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
*/
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No current backup version",
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No current backup version",
},
},
});
{ name: "room-keys-version" },
);
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
overwriteRoutes: true,
});
return {
version: backupVersion,
};
});
fetchMock.post(
"path:/_matrix/client/v3/room_keys/version",
(callLog) => {
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
fetchMock.modifyRoute("room-keys-version", { response: backupData });
return {
version: backupVersion,
};
},
{ name: "post-room-keys-version" },
);
}
+4 -4
View File
@@ -82,7 +82,7 @@ export function getSyncResponse(
state_key: "",
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"io.element.msc3414.encrypt_state_events": encryptStateEvents,
"io.element.msc4362.encrypt_state_events": encryptStateEvents,
},
}),
],
@@ -136,7 +136,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
// eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
result[key] = vi.fn();
}
} catch {
// Direct access to some non-function fields of DOM prototypes may
@@ -592,7 +592,7 @@ export async function advanceTimersUntil<T>(promise: Promise<T>): Promise<T> {
});
while (!resolved) {
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
}
return await promise;
@@ -641,7 +641,7 @@ export function waitFor<T>(
checkCallback();
while (!finished) {
jest.advanceTimersByTime(interval);
vi.advanceTimersByTime(interval);
// Could have timed-out
if (finished) break;
+51 -92
View File
@@ -17,21 +17,17 @@ limitations under the License.
import {
type ClientEvent,
type ClientEventHandlerMap,
type EmptyObject,
EventType,
type GroupCall,
GroupCallIntent,
GroupCallType,
type IContent,
type ISendEventResponse,
type MatrixClient,
type MatrixEvent,
type Room,
RoomMember,
type RoomState,
RoomStateEvent,
type RoomStateEventHandlerMap,
type SendToDeviceContentMap,
} from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
@@ -269,19 +265,20 @@ export class MockRTCRtpTransceiver {
this.peerConn.needsNegotiation = true;
}
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
public setCodecPreferences = vi.fn<RTCRtpTransceiver["setCodecPreferences"]>();
}
export class MockMediaStreamTrack {
export class MockMediaStreamTrack extends EventTarget {
constructor(
public readonly id: string,
public readonly kind: "audio" | "video",
public enabled = true,
) {}
) {
super();
}
public stop = jest.fn<void, []>();
public stop = vi.fn<() => void>();
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public settings?: MediaTrackSettings;
@@ -289,45 +286,21 @@ export class MockMediaStreamTrack {
return this.settings!;
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
public typed(): MediaStreamTrack {
return this as unknown as MediaStreamTrack;
}
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
export class MockMediaStream {
export class MockMediaStream extends EventTarget {
constructor(
public id: string,
private tracks: MockMediaStreamTrack[] = [],
) {}
) {
super();
}
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public getTracks() {
return this.tracks;
}
@@ -337,17 +310,9 @@ export class MockMediaStream {
public getVideoTracks() {
return this.tracks.filter((track) => track.kind === "video");
}
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
public addTrack(track: MockMediaStreamTrack) {
this.tracks.push(track);
this.dispatchEvent("addtrack");
this.dispatchEvent(new Event("addtrack"));
}
public removeTrack(track: MockMediaStreamTrack) {
this.tracks.splice(this.tracks.indexOf(track), 1);
@@ -391,7 +356,7 @@ export class MockMediaHandler {
public stopUserMediaStream(stream: MockMediaStream) {
stream.isStopped = true;
}
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
public getScreensharingStream = vi.fn((opts?: IScreensharingOpts) => {
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
@@ -416,19 +381,19 @@ export class MockMediaHandler {
}
export class MockMediaDevices {
public enumerateDevices = jest
.fn<Promise<MediaDeviceInfo[]>, []>()
public enumerateDevices = vi
.fn<MediaDevices["enumerateDevices"]>()
.mockResolvedValue([
new MockMediaDeviceInfo("audioinput").typed(),
new MockMediaDeviceInfo("videoinput").typed(),
]);
public getUserMedia = jest
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
public getUserMedia = vi
.fn<MediaDevices["getUserMedia"]>()
.mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
public getDisplayMedia = jest
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
public getDisplayMedia = vi
.fn<MediaDevices["getDisplayMedia"]>()
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
public typed(): MediaDevices {
@@ -462,14 +427,8 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
calls: new Map<string, MatrixCall>(),
};
public sendStateEvent = jest.fn<
Promise<ISendEventResponse>,
[roomId: string, eventType: EventType, content: any, statekey: string]
>();
public sendToDevice = jest.fn<
Promise<EmptyObject>,
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
>();
public sendStateEvent = vi.fn<MatrixClient["sendStateEvent"]>();
public sendToDevice = vi.fn<MatrixClient["sendToDevice"]>();
public isInitialSyncComplete(): boolean {
return false;
@@ -499,11 +458,11 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getUseE2eForGroupCall = () => false;
public checkTurnServers = () => null;
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
public getSyncState = vi.fn<MatrixClient["getSyncState"]>().mockReturnValue(SyncState.Syncing);
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();
public getFoci = jest.fn();
public getRooms = vi.fn<MatrixClient["getRooms"]>().mockReturnValue([]);
public getRoom = vi.fn();
public getFoci = vi.fn();
public supportsThreads(): boolean {
return true;
@@ -534,20 +493,20 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
isAudioMuted: jest.fn().mockReturnValue(false),
isVideoMuted: jest.fn().mockReturnValue(false),
setAudioVideoMuted: vi.fn<CallFeed["setAudioVideoMuted"]>(),
isAudioMuted: vi.fn().mockReturnValue(false),
isVideoMuted: vi.fn().mockReturnValue(false),
stream: new MockMediaStream("stream"),
} as unknown as CallFeed;
public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
public hangup = jest.fn<void, []>();
public initStats = jest.fn<void, []>();
public reject = vi.fn<() => void>();
public answerWithCallFeeds = vi.fn<MatrixCall["answerWithCallFeeds"]>();
public hangup = vi.fn<() => void>();
public initStats = vi.fn<() => void>();
public sendMetadataUpdate = jest.fn<void, []>();
public sendMetadataUpdate = vi.fn<() => void>();
public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember;
@@ -586,11 +545,11 @@ export class MockCallFeed {
}
export function installWebRTCMocks() {
globalThis.navigator = {
vi.stubGlobal("navigator", {
mediaDevices: new MockMediaDevices().typed(),
} as unknown as Navigator;
});
globalThis.window = {
vi.stubGlobal("window", {
// @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock
@@ -598,16 +557,16 @@ export function installWebRTCMocks() {
// @ts-ignore Mock
RTCIceCandidate: {},
getUserMedia: () => new MockMediaStream("local_stream"),
};
// @ts-ignore Mock
globalThis.document = {};
});
vi.stubGlobal("document", {});
// @ts-ignore Mock
globalThis.AudioContext = MockAudioContext;
// @ts-ignore Mock
globalThis.RTCRtpReceiver = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
getCapabilities: vi.fn().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
@@ -615,7 +574,7 @@ export function installWebRTCMocks() {
// @ts-ignore Mock
globalThis.RTCRtpSender = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
getCapabilities: vi.fn().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
@@ -632,22 +591,22 @@ export function makeMockGroupCallStateEvent(
redacted?: boolean,
): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue(content),
getStateKey: jest.fn().mockReturnValue(groupCallId),
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
getType: vi.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: vi.fn().mockReturnValue(roomId),
getTs: vi.fn().mockReturnValue(0),
getContent: vi.fn().mockReturnValue(content),
getStateKey: vi.fn().mockReturnValue(groupCallId),
isRedacted: vi.fn().mockReturnValue(redacted ?? false),
} as unknown as MatrixEvent;
}
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue({}),
getStateKey: jest.fn().mockReturnValue(groupCallId),
getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getRoomId: vi.fn().mockReturnValue(roomId),
getTs: vi.fn().mockReturnValue(0),
getContent: vi.fn().mockReturnValue({}),
getStateKey: vi.fn().mockReturnValue(groupCallId),
} as unknown as MatrixEvent;
}
+1 -1
View File
@@ -61,7 +61,7 @@ describe("UnstableValue", () => {
it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null!, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(<any>ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
});
+2 -2
View File
@@ -38,7 +38,7 @@ describe("ReEmitter", function () {
const src = new EventSource();
const tgt = new EventTarget();
const handler = jest.fn();
const handler = vi.fn();
tgt.on(EVENTNAME, handler);
const reEmitter = new ReEmitter(tgt);
@@ -61,7 +61,7 @@ describe("ReEmitter", function () {
// without the workaround in ReEmitter, this would throw
src.doAnError();
const handler = jest.fn();
const handler = vi.fn();
tgt.on("error", handler);
src.doAnError();
+3 -3
View File
@@ -35,16 +35,16 @@ describe("onResumedSync", () => {
};
store = new StubStore();
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
store.getOldestToDeviceBatch = vi.fn().mockImplementation(() => {
return batch;
});
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
store.removeToDeviceBatch = vi.fn().mockImplementation(() => {
batch = null;
});
mockClient = getMockClientWithEventEmitter({});
mockClient.store = store;
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
mockClient.sendToDevice = vi.fn().mockImplementation(async () => {
if (shouldFailSendToDevice) {
await Promise.reject(new ConnectionError("")).finally(() => {
setTimeout(onSendToDeviceFailure, 0);
+2 -1
View File
@@ -35,6 +35,7 @@ describe("AutoDiscovery", function () {
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
});
// eslint-disable-next-line @vitest/expect-expect
it("should throw an error when no domain is specified", function () {
getHttpBackend();
return Promise.all([
@@ -190,7 +191,7 @@ describe("AutoDiscovery", function () {
};
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then(expect(expected).toEqual),
AutoDiscovery.findClientConfig("example.org").then((config) => expect(config).toEqual(expected)),
]);
});
+2 -2
View File
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
describe("makeBeaconInfoContent()", () => {
const mockDateNow = 123456789;
beforeEach(() => {
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
vi.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
});
afterAll(() => {
jest.spyOn(globalThis.Date, "now").mockRestore();
vi.spyOn(globalThis.Date, "now").mockRestore();
});
it("create fully defined event content", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
+4 -1
View File
@@ -14,8 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @vitest-environment happy-dom
*/
import "fake-indexeddb/auto";
import "jest-localstorage-mock";
import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src";
import { type CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
+2 -7
View File
@@ -29,12 +29,7 @@ describe("sha256", () => {
});
it("throws if webcrypto is not available", async () => {
const oldCrypto = globalThis.crypto;
try {
globalThis.crypto = {} as any;
await expect(sha256("test")).rejects.toThrow();
} finally {
globalThis.crypto = oldCrypto;
}
vi.stubGlobal("crypto", {});
await expect(sha256("test")).rejects.toThrow();
});
});
+97 -50
View File
@@ -1,7 +1,3 @@
/**
* @jest-environment jsdom
*/
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
@@ -22,7 +18,7 @@ limitations under the License.
// project, which doesn't know about our TypeEventEmitter implementation at all
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { type MockedObject } from "jest-mock";
import { type MockedObject } from "vitest";
import {
type WidgetApi,
WidgetApiToWidgetAction,
@@ -54,26 +50,26 @@ const testOIDCToken = {
token_type: "Bearer",
};
class MockWidgetApi extends EventEmitter {
public start = jest.fn().mockResolvedValue(undefined);
public getClientVersions = jest.fn();
public requestCapability = jest.fn().mockResolvedValue(undefined);
public requestCapabilities = jest.fn().mockResolvedValue(undefined);
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendEvent = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveEvent = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendMessage = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveMessage = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendState = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveState = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendToDevice = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveToDevice = jest.fn().mockResolvedValue(undefined);
public sendRoomEvent = jest.fn(
public start = vi.fn().mockResolvedValue(undefined);
public getClientVersions = vi.fn();
public requestCapability = vi.fn().mockResolvedValue(undefined);
public requestCapabilities = vi.fn().mockResolvedValue(undefined);
public requestCapabilityForRoomTimeline = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendEvent = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveEvent = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendMessage = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveMessage = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendState = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveState = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToSendToDevice = vi.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveToDevice = vi.fn().mockResolvedValue(undefined);
public sendRoomEvent = vi.fn(
async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public sendStateEvent = jest.fn(
public sendStateEvent = vi.fn(
async (
eventType: string,
stateKey: string,
@@ -86,22 +82,24 @@ class MockWidgetApi extends EventEmitter {
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public updateDelayedEvent = jest.fn().mockResolvedValue(undefined);
public sendToDevice = jest.fn().mockResolvedValue(undefined);
public requestOpenIDConnectToken = jest.fn(async () => {
public cancelScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
public restartScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
public sendScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
public sendToDevice = vi.fn().mockResolvedValue(undefined);
public requestOpenIDConnectToken = vi.fn(async () => {
return testOIDCToken;
return new Promise<IOpenIDCredentials>(() => {
return testOIDCToken;
});
});
public readStateEvents = jest.fn(async () => []);
public getTurnServers = jest.fn(async () => []);
public sendContentLoaded = jest.fn().mockResolvedValue(undefined);
public readStateEvents = vi.fn(async () => []);
public getTurnServers = vi.fn(async () => []);
public sendContentLoaded = vi.fn().mockResolvedValue(undefined);
public transport = {
reply: jest.fn(),
send: jest.fn(),
sendComplete: jest.fn(),
reply: vi.fn(),
send: vi.fn(),
sendComplete: vi.fn(),
};
/**
@@ -228,7 +226,7 @@ describe("RoomWidgetClient", () => {
);
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
const injectSpy = jest.spyOn((client as any).syncApi, "injectRoomEvents");
const injectSpy = vi.spyOn((client as any).syncApi, "injectRoomEvents");
const widgetSendEmitter = new EventEmitter();
const widgetSendPromise = new Promise<void>((resolve) =>
widgetSendEmitter.once("send", () => resolve()),
@@ -355,10 +353,10 @@ describe("RoomWidgetClient", () => {
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to send");
widgetApi.transport.send.mockRejectedValue(error);
vi.mocked(widgetApi.transport.send).mockRejectedValue(error);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
@@ -381,22 +379,22 @@ describe("RoomWidgetClient", () => {
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl, undefined, expect.any(Headers));
widgetApi.transport.send.mockRejectedValue(widgetError);
vi.mocked(widgetApi.transport.send).mockRejectedValue(widgetError);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow(matrixError);
).rejects.toStrictEqual(matrixError);
});
});
describe("delayed events", () => {
describe("when supported", () => {
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"),
);
@@ -531,21 +529,63 @@ describe("RoomWidgetClient", () => {
).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"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await client._unstable_updateDelayedEvent("id", action);
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
}
await client._unstable_cancelScheduledDelayedEvent("id");
expect(widgetApi.cancelScheduledDelayedEvent).toHaveBeenCalledWith("id");
});
it("can restart 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_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", () => {
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
beforeAll(() => {
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
});
afterAll(() => {
doesServerSupportUnstableFeatureMock.mockReset();
});
it("fails to send delayed message events", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
await expect(
@@ -583,6 +623,13 @@ describe("RoomWidgetClient", () => {
"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 +776,7 @@ describe("RoomWidgetClient", () => {
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
const logSpy = jest.spyOn(logger, "error");
const logSpy = vi.spyOn(logger, "error");
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
@@ -929,7 +976,7 @@ describe("RoomWidgetClient", () => {
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to get token");
widgetApi.transport.sendComplete.mockRejectedValue(error);
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(error);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
@@ -953,9 +1000,9 @@ describe("RoomWidgetClient", () => {
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl, undefined, expect.any(Headers));
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(widgetError);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
+3 -3
View File
@@ -37,7 +37,7 @@ describe("eventMapperFor", function () {
setUserCreator(_) {},
} as IStore,
scheduler: {
setProcessFunction: jest.fn(),
setProcessFunction: vi.fn(),
} as unknown as MatrixScheduler,
userId: userId,
});
@@ -133,7 +133,7 @@ describe("eventMapperFor", function () {
event_id: eventId,
};
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
const decryptEventIfNeededSpy = vi.spyOn(client, "decryptEventIfNeeded");
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
const mapper = eventMapperFor(client, {
@@ -161,7 +161,7 @@ describe("eventMapperFor", function () {
event_id: eventId,
};
const evListener = jest.fn();
const evListener = vi.fn();
client.on(MatrixEventEvent.Replaced, evListener);
const noReEmitMapper = eventMapperFor(client, {
+12 -12
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { type MockInstance } from "vitest";
import * as utils from "../test-utils/test-utils";
import {
@@ -196,7 +196,7 @@ describe("EventTimelineSet", () => {
it("should aggregate relations which belong to unknown timeline without adding them to any timeline", () => {
// If threads are disabled all events go into the main timeline
mocked(client.supportsThreads).mockReturnValue(true);
vi.mocked(client.supportsThreads).mockReturnValue(true);
const reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
const liveTimeline = eventTimelineSet.getLiveTimeline();
@@ -228,7 +228,7 @@ describe("EventTimelineSet", () => {
let thread: Thread;
beforeEach(() => {
(client.supportsThreads as jest.Mock).mockReturnValue(true);
vi.mocked(client.supportsThreads).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
@@ -306,20 +306,20 @@ describe("EventTimelineSet", () => {
});
describe("with events to be decrypted", () => {
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
let messageEventShouldAttemptDecryptionSpy: MockInstance;
let messageEventIsDecryptionFailureSpy: MockInstance;
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance;
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
let replyEventShouldAttemptDecryptionSpy: MockInstance;
let replyEventIsDecryptionFailureSpy: MockInstance;
beforeEach(() => {
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption");
messageEventShouldAttemptDecryptionSpy = vi.spyOn(messageEvent, "shouldAttemptDecryption");
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
messageEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, "shouldAttemptDecryption");
replyEventShouldAttemptDecryptionSpy = vi.spyOn(replyEvent, "shouldAttemptDecryption");
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
replyEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
});
@@ -384,7 +384,7 @@ describe("EventTimelineSet", () => {
let thread: Thread;
beforeEach(() => {
(client.supportsThreads as jest.Mock).mockReturnValue(true);
vi.mocked(client.supportsThreads).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
+12 -14
View File
@@ -1,5 +1,3 @@
import { mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils";
import { Direction, EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
@@ -20,21 +18,21 @@ describe("EventTimeline", function () {
const getTimeline = (): EventTimeline => {
const room = new Room(roomId, mockClient, userA);
const timelineSet = new EventTimelineSet(room);
jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
vi.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
const timeline = new EventTimeline(timelineSet);
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
// otherwise the default member property values (e.g. paginationToken) will be incorrect
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
timeline.getState(Direction.Backward)!.setStateEvents = vi.fn();
timeline.getState(Direction.Backward)!.getSentinelMember = vi.fn();
timeline.getState(Direction.Forward)!.setStateEvents = vi.fn();
timeline.getState(Direction.Forward)!.getSentinelMember = vi.fn();
return timeline;
};
beforeEach(function () {
// reset any RoomState mocks
jest.resetAllMocks();
vi.resetAllMocks();
timeline = getTimeline();
});
@@ -67,12 +65,12 @@ describe("EventTimeline", function () {
timeline.initialiseState(events);
// @ts-ignore private prop
const timelineStartState = timeline.startState!;
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
expect(vi.mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
timelineWasEmpty: undefined,
});
// @ts-ignore private prop
const timelineEndState = timeline.endState!;
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
expect(vi.mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
timelineWasEmpty: undefined,
});
});
@@ -210,13 +208,13 @@ describe("EventTimeline", function () {
sentinel.name = "Old Alice";
sentinel.membership = KnownMembership.Join;
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
return oldSentinel;
}
@@ -253,13 +251,13 @@ describe("EventTimeline", function () {
sentinel.name = "Old Alice";
sentinel.membership = KnownMembership.Join;
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
return oldSentinel;
}
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://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",
"params": {
+1 -1
View File
@@ -57,7 +57,7 @@ describe("MatrixError", () => {
it("should retrieve Date Retry-After header from rate-limit error", () => {
headers.set("Retry-After", `${new Date(160000).toUTCString()}`);
jest.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
vi.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
// prefer Retry-After header over retry_after_ms
+83 -75
View File
@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { Mocked, MockedFunction } from "jest-mock";
import { type Mocked, type MockedFunction } from "vitest";
import { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import {
@@ -26,6 +27,7 @@ import {
type IHttpOpts,
MatrixError,
Method,
TokenRefreshError,
} from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
import { type QueryDict, sleep } from "../../../src/utils";
@@ -38,7 +40,7 @@ describe("FetchHttpApi", () => {
const tokenInactiveError = new MatrixError({ errcode: "M_UNKNOWN_TOKEN", error: "Token is not active" }, 401);
beforeEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});
it("should support aborting multiple times", () => {
@@ -47,29 +49,29 @@ describe("FetchHttpApi", () => {
api.request(Method.Get, "/foo");
api.request(Method.Get, "/baz");
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
expect((fetchFn.mock.calls[0][0] as URL).href.endsWith("/foo")).toBeTruthy();
expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeFalsy();
expect((fetchFn.mock.calls[1][0] as URL).href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeTruthy();
api.request(Method.Get, "/bar");
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
expect((fetchFn.mock.calls[2][0] as URL).href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeTruthy();
});
it("should fall back to global fetch if fetchFn not provided", () => {
globalThis.fetch = jest.fn();
expect(globalThis.fetch).not.toHaveBeenCalled();
const spy = (globalThis.fetch = vi.fn());
expect(spy).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
api.fetch("test");
expect(globalThis.fetch).toHaveBeenCalled();
expect(spy).toHaveBeenCalled();
});
it("should update identity server base url", () => {
@@ -97,8 +99,8 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).toBe("bar");
expect((fetchFn.mock.calls[0][0] as URL).searchParams.getAll("via")).toEqual(["a", "b"]);
});
it("should send params as body for non-GET requests", () => {
@@ -112,8 +114,8 @@ describe("FetchHttpApi", () => {
});
const params = { foo: "bar", via: ["a", "b"] };
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1]!.body as string)).toStrictEqual(params);
});
it("should add Authorization header if token provided", () => {
@@ -126,7 +128,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>).Authorization).toBe("Bearer token");
});
});
@@ -142,7 +144,7 @@ describe("FetchHttpApi", () => {
it("should set an Accept header, and parse the response as JSON, by default", async () => {
const result = { a: 1 };
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue(result) });
const fetchFn = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue(result) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(result);
expect(fetchFn.mock.calls[0][1].headers.Accept).toBe("application/json");
@@ -150,7 +152,7 @@ describe("FetchHttpApi", () => {
it("should not set an Accept header, and should return text if json=false", async () => {
const text = "418 I'm a teapot";
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
const fetchFn = vi.fn().mockResolvedValue({ ok: true, text: vi.fn().mockResolvedValue(text) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(
api.requestOtherUrl(Method.Get, "http://url", undefined, {
@@ -162,7 +164,7 @@ describe("FetchHttpApi", () => {
it("should not set an Accept header, and should return a blob, if rawResponseBody is true", async () => {
const blob = new Blob(["blobby"]);
const fetchFn = jest.fn().mockResolvedValue({ ok: true, blob: jest.fn().mockResolvedValue(blob) });
const fetchFn = vi.fn().mockResolvedValue({ ok: true, blob: vi.fn().mockResolvedValue(blob) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(
api.requestOtherUrl(Method.Get, "http://url", undefined, {
@@ -176,7 +178,7 @@ describe("FetchHttpApi", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn: jest.fn(),
fetchFn: vi.fn(),
onlyData: true,
});
await expect(
@@ -195,7 +197,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
await api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBe("token");
});
it("should send token via headers by default", async () => {
@@ -208,7 +210,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
await api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer token");
});
it("should not send a token if not calling `authedRequest`", () => {
@@ -221,8 +223,8 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
api.request(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBeFalsy();
});
it("should ensure no token is leaked out via query params if sending via headers", async () => {
@@ -236,8 +238,8 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
await api.authedRequest(Method.Get, "/path", { access_token: "123" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer token");
});
it("should not override manually specified access token via query params", async () => {
@@ -251,7 +253,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
await api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBe("RealToken");
});
it("should not override manually specified access token via header", async () => {
@@ -267,7 +269,7 @@ describe("FetchHttpApi", () => {
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Authorization: "Bearer RealToken" },
});
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer RealToken");
});
it("should not override Accept header", async () => {
@@ -276,18 +278,18 @@ describe("FetchHttpApi", () => {
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Accept: "text/html" },
});
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Accept"]).toBe("text/html");
});
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
const fetchFn = jest.fn().mockResolvedValue({
const fetchFn = vi.fn().mockResolvedValue({
ok: false,
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(
text: vi.fn().mockResolvedValue(
JSON.stringify({
errcode: "M_CONSENT_NOT_GIVEN",
error: "Ye shall ask for consent",
@@ -309,7 +311,7 @@ describe("FetchHttpApi", () => {
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, onlyData: true });
await api.authedRequest(Method.Post, "/account/password");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>).Authorization).toBeUndefined();
});
describe("with refresh token", () => {
@@ -322,7 +324,13 @@ describe("FetchHttpApi", () => {
error: "Token is not active",
soft_logout: false,
};
const unknownTokenErr = new MatrixError(unknownTokenErrBody, 401);
const unknownTokenErr = new MatrixError(
unknownTokenErrBody,
401,
undefined,
undefined,
expect.anything(),
);
const unknownTokenResponse = {
ok: false,
status: 401,
@@ -331,19 +339,19 @@ describe("FetchHttpApi", () => {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
text: vi.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
};
const okayResponse = {
ok: true,
status: 200,
json: jest.fn().mockResolvedValue({ x: 1 }),
json: vi.fn().mockResolvedValue({ x: 1 }),
};
describe("without a tokenRefreshFunction", () => {
it("should emit logout and throw", async () => {
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -362,10 +370,10 @@ describe("FetchHttpApi", () => {
describe("with a tokenRefreshFunction", () => {
it("should emit logout and throw when token refresh fails", async () => {
const error = new MatrixError();
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -384,10 +392,10 @@ describe("FetchHttpApi", () => {
it("should not emit logout but still throw when token refresh fails due to transitive fault", async () => {
const error = new ConnectionError("transitive fault");
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -398,7 +406,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
unknownTokenErr,
new TokenRefreshError(unknownTokenErr),
);
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
@@ -407,16 +415,16 @@ describe("FetchHttpApi", () => {
it("should refresh token and retry request", async () => {
const newAccessToken = "new-access-token";
const newRefreshToken = "new-refresh-token";
const tokenRefreshFunction = jest.fn().mockResolvedValue({
const tokenRefreshFunction = vi.fn().mockResolvedValue({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
});
const fetchFn = jest
const fetchFn = vi
.fn()
.mockResolvedValueOnce(unknownTokenResponse)
.mockResolvedValueOnce(okayResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -448,7 +456,7 @@ describe("FetchHttpApi", () => {
// count because it's only to get a token with an expiry)
const newAccessToken = "new-access-token";
const newRefreshToken = "new-refresh-token";
const tokenRefreshFunction = jest.fn().mockReturnValue({
const tokenRefreshFunction = vi.fn().mockReturnValue({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
// This needs to be sufficiently high that it's over the threshold for
@@ -457,10 +465,10 @@ describe("FetchHttpApi", () => {
});
// fetch doesn't like our new or old tokens
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -470,7 +478,7 @@ describe("FetchHttpApi", () => {
refreshToken,
onlyData: true,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
unknownTokenErr,
);
@@ -492,7 +500,7 @@ describe("FetchHttpApi", () => {
// first refresh is to get a token with an expiry at all, because we
// can't specify an expiry on the token we inject
const tokenRefreshFunction = jest.fn().mockResolvedValueOnce({
const tokenRefreshFunction = vi.fn().mockResolvedValueOnce({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiry: new Date(Date.now() + 1000),
@@ -513,10 +521,10 @@ describe("FetchHttpApi", () => {
expiry: new Date(Date.now() + 5 * 60 * 1000),
});
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
vi.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
@@ -526,7 +534,7 @@ describe("FetchHttpApi", () => {
refreshToken,
onlyData: true,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
unknownTokenErr,
);
@@ -543,7 +551,7 @@ describe("FetchHttpApi", () => {
const localBaseUrl = "http://baseurl";
const baseUrlWithTrailingSlash = "http://baseurl/";
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
const fetchFn = jest.fn();
const fetchFn = vi.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn, onlyData: true });
};
@@ -596,7 +604,7 @@ describe("FetchHttpApi", () => {
describe("extraParams handling", () => {
const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => {
const fetchFn = jest.fn();
const fetchFn = vi.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
return new FetchHttpApi(emitter, {
baseUrl: localBaseUrl,
@@ -655,7 +663,7 @@ describe("FetchHttpApi", () => {
});
it("should work when extraParams is undefined", () => {
const fetchFn = jest.fn();
const fetchFn = vi.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, onlyData: true });
@@ -679,11 +687,11 @@ describe("FetchHttpApi", () => {
});
it("should not log query parameters", async () => {
jest.useFakeTimers();
vi.useFakeTimers();
const responseResolvers = Promise.withResolvers<Response>();
const fetchFn = jest.fn().mockReturnValue(responseResolvers.promise);
const fetchFn = vi.fn().mockReturnValue(responseResolvers.promise);
const mockLogger = {
debug: jest.fn(),
debug: vi.fn(),
} as unknown as Mocked<Logger>;
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
@@ -693,7 +701,7 @@ describe("FetchHttpApi", () => {
onlyData: true,
});
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
jest.advanceTimersByTime(1234);
vi.advanceTimersByTime(1234);
responseResolvers.resolve({ ok: true, status: 200, json: () => Promise.resolve("RESPONSE") } as Response);
await prom;
expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
@@ -714,7 +722,7 @@ describe("FetchHttpApi", () => {
it("should not make multiple concurrent refresh token requests", async () => {
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
const fetchFn = jest.fn().mockResolvedValue({
const fetchFn = vi.fn().mockResolvedValue({
ok: false,
status: tokenInactiveError.httpStatus,
async text() {
@@ -724,10 +732,10 @@ describe("FetchHttpApi", () => {
return tokenInactiveError.data;
},
headers: {
get: jest.fn().mockReturnValue("application/json"),
get: vi.fn().mockReturnValue("application/json"),
},
});
const tokenRefreshFunction = jest.fn().mockReturnValue(deferredTokenRefresh.promise);
const tokenRefreshFunction = vi.fn().mockReturnValue(deferredTokenRefresh.promise);
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
@@ -755,7 +763,7 @@ describe("FetchHttpApi", () => {
return {};
},
headers: {
get: jest.fn().mockReturnValue("application/json"),
get: vi.fn().mockReturnValue("application/json"),
},
});
deferredTokenRefresh.resolve({ accessToken: "NEW_ACCESS_TOKEN", refreshToken: "NEW_REFRESH_TOKEN" });
@@ -770,7 +778,7 @@ describe("FetchHttpApi", () => {
it("should use newly refreshed token if request starts mid-refresh", async () => {
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
const fetchFn = jest.fn().mockResolvedValue({
const fetchFn = vi.fn().mockResolvedValue({
ok: false,
status: tokenInactiveError.httpStatus,
async text() {
@@ -780,10 +788,10 @@ describe("FetchHttpApi", () => {
return tokenInactiveError.data;
},
headers: {
get: jest.fn().mockReturnValue("application/json"),
get: vi.fn().mockReturnValue("application/json"),
},
});
const tokenRefreshFunction = jest.fn().mockReturnValue(deferredTokenRefresh.promise);
const tokenRefreshFunction = vi.fn().mockReturnValue(deferredTokenRefresh.promise);
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
@@ -813,7 +821,7 @@ describe("FetchHttpApi", () => {
return {};
},
headers: {
get: jest.fn().mockReturnValue("application/json"),
get: vi.fn().mockReturnValue("application/json"),
},
});
@@ -832,6 +840,6 @@ describe("FetchHttpApi", () => {
});
});
function makeMockFetchFn(): MockedFunction<any> {
return jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
function makeMockFetchFn(): MockedFunction<Window["fetch"]> {
return vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
}
+72 -61
View File
@@ -14,54 +14,59 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { ClientPrefix, MatrixHttpApi, Method, type UploadResponse } from "../../../src";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
jest.useFakeTimers();
vi.useFakeTimers();
describe("MatrixHttpApi", () => {
const baseUrl = "http://baseUrl";
const prefix = ClientPrefix.V3;
let xhr: Writeable<XMLHttpRequest>;
let upload: Promise<UploadResponse>;
const DONE = 0;
function getRequest(): Writeable<XMLHttpRequest> | undefined {
return vi.mocked(globalThis.XMLHttpRequest)?.mock.instances.at(-1);
}
beforeEach(() => {
xhr = {
upload: {} as XMLHttpRequestUpload,
open: jest.fn(),
send: jest.fn(),
abort: jest.fn(),
setRequestHeader: jest.fn(),
onreadystatechange: undefined,
getResponseHeader: jest.fn(),
getAllResponseHeaders: jest.fn(),
} as unknown as XMLHttpRequest;
// We stub out XHR here as it is not available in JSDOM
// We stub out XHR here as it is not available in the test environment
// @ts-ignore
globalThis.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
globalThis.XMLHttpRequest = vi.fn().mockImplementation(function (this: XMLHttpRequest) {
// @ts-ignore
this.upload = {} as XMLHttpRequestUpload;
this.open = vi.fn();
this.send = vi.fn();
this.abort = vi.fn();
this.setRequestHeader = vi.fn();
// @ts-ignore
this.onreadystatechange = undefined;
this.getResponseHeader = vi.fn();
this.getAllResponseHeaders = vi.fn();
});
// @ts-ignore
globalThis.XMLHttpRequest.DONE = DONE;
});
afterEach(() => {
upload?.catch(() => {});
// Abort any remaining requests
xhr.readyState = DONE;
xhr.status = 0;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
const xhr = getRequest();
if (xhr) {
// Abort any remaining requests
xhr.readyState = DONE;
xhr.status = 0;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
}
});
it("should fall back to `fetch` where xhr is unavailable", async () => {
globalThis.XMLHttpRequest = undefined!;
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
const fetchFn = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
upload = api.uploadContent({} as File);
await upload;
@@ -69,11 +74,11 @@ describe("MatrixHttpApi", () => {
});
it("should prefer xhr where available", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const fetchFn = vi.fn().mockResolvedValue({ ok: true });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
upload = api.uploadContent({} as File);
expect(fetchFn).not.toHaveBeenCalled();
expect(xhr.open).toHaveBeenCalled();
expect(getRequest()!.open).toHaveBeenCalled();
});
it("should send access token in query params if header disabled", () => {
@@ -85,11 +90,11 @@ describe("MatrixHttpApi", () => {
onlyData: true,
});
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(
expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token",
);
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
expect(getRequest()!.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
});
it("should send access token in header by default", () => {
@@ -100,14 +105,17 @@ describe("MatrixHttpApi", () => {
onlyData: true,
});
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
);
expect(getRequest()!.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
});
it("should include filename by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File, { name: "name" });
expect(xhr.open).toHaveBeenCalledWith(
expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
);
@@ -116,42 +124,45 @@ describe("MatrixHttpApi", () => {
it("should allow not sending the filename", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
expect(getRequest()!.open).toHaveBeenCalledWith(
Method.Post,
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
);
});
it("should abort xhr when the upload is aborted", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
api.cancelUpload(upload);
expect(xhr.abort).toHaveBeenCalled();
expect(getRequest()!.abort).toHaveBeenCalled();
return expect(upload).rejects.toThrow("Aborted");
});
it("should timeout if no progress in 30s", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
jest.advanceTimersByTime(25000);
vi.advanceTimersByTime(25000);
// @ts-ignore
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
jest.advanceTimersByTime(25000);
expect(xhr.abort).not.toHaveBeenCalled();
jest.advanceTimersByTime(5000);
expect(xhr.abort).toHaveBeenCalled();
getRequest()!.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
vi.advanceTimersByTime(25000);
expect(getRequest()!.abort).not.toHaveBeenCalled();
vi.advanceTimersByTime(5000);
expect(getRequest()!.abort).toHaveBeenCalled();
});
it("should call progressHandler", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
const progressHandler = jest.fn();
const progressHandler = vi.fn();
upload = api.uploadContent({} as File, { progressHandler });
const progressEvent = new Event("progress") as ProgressEvent;
Object.assign(progressEvent, { loaded: 1, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
getRequest()!.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
Object.assign(progressEvent, { loaded: 95, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
getRequest()!.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
});
@@ -159,11 +170,11 @@ describe("MatrixHttpApi", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = "";
xhr.status = 200;
getRequest()!.readyState = DONE;
getRequest()!.responseText = "";
getRequest()!.status = 200;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
getRequest()!.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("No response body.");
});
@@ -172,15 +183,15 @@ describe("MatrixHttpApi", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
xhr.status = 404;
mocked(xhr.getResponseHeader).mockImplementation((name) =>
getRequest()!.readyState = DONE;
getRequest()!.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
getRequest()!.status = 404;
vi.mocked(getRequest()!.getResponseHeader).mockImplementation((name) =>
name.toLowerCase() === "content-type" ? "application/json" : null,
);
mocked(xhr.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
vi.mocked(getRequest()!.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
getRequest()!.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("Not found");
});
@@ -189,12 +200,12 @@ describe("MatrixHttpApi", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
xhr.status = 200;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
getRequest()!.readyState = DONE;
getRequest()!.responseText = '{"content_uri": "mxc://server/foobar"}';
getRequest()!.status = 200;
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
getRequest()!.onreadystatechange?.(new Event("test"));
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
});
@@ -203,22 +214,22 @@ describe("MatrixHttpApi", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
expect(api.cancelUpload(upload)).toBeTruthy();
expect(xhr.abort).toHaveBeenCalled();
expect(getRequest()!.abort).toHaveBeenCalled();
});
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.status = 500;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
getRequest()!.readyState = DONE;
getRequest()!.status = 500;
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
getRequest()!.onreadystatechange?.(new Event("test"));
await upload.catch(() => {});
expect(api.cancelUpload(upload)).toBeFalsy();
expect(xhr.abort).not.toHaveBeenCalled();
expect(getRequest()!.abort).not.toHaveBeenCalled();
});
it("should return active uploads in `getCurrentUploads`", () => {
+72 -27
View File
@@ -14,51 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import {
anySignal,
ConnectionError,
HTTPError,
MatrixError,
MatrixSafetyError,
MatrixSafetyErrorCode,
parseErrorResponse,
retryNetworkOperation,
timeoutSignal,
} from "../../../src";
import { sleep } from "../../../src/utils";
jest.mock("../../../src/utils");
vi.mock("../../../src/utils");
// setupTests mocks `timeoutSignal` due to hanging timers
jest.unmock("../../../src/http-api/utils");
vi.unmock("../../../src/http-api/utils");
describe("timeoutSignal", () => {
jest.useFakeTimers();
vi.useFakeTimers();
it("should fire abort signal after specified timeout", () => {
const signal = timeoutSignal(3000);
const onabort = jest.fn();
const onabort = vi.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(3000);
vi.advanceTimersByTime(3000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
});
describe("anySignal", () => {
jest.useFakeTimers();
vi.useFakeTimers();
it("should fire when any signal fires", () => {
const { signal } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
const onabort = jest.fn();
const onabort = vi.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(2000);
vi.advanceTimersByTime(2000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
@@ -66,13 +66,13 @@ describe("anySignal", () => {
it("should cleanup when instructed", () => {
const { signal, cleanup } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
const onabort = jest.fn();
const onabort = vi.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
cleanup();
jest.advanceTimersByTime(2000);
vi.advanceTimersByTime(2000);
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
});
@@ -86,9 +86,14 @@ describe("anySignal", () => {
});
describe("parseErrorResponse", () => {
const url = "https://example.org";
let headers: Headers;
const xhrHeaderMethods = {
getResponseHeader: (name: string) => headers.get(name),
responseURL: url,
getResponseHeader: (name: string) => {
headers.get(name);
},
getAllResponseHeaders: () => {
let allHeaders = "";
headers.forEach((value, key) => {
@@ -118,6 +123,9 @@ describe("parseErrorResponse", () => {
errcode: "TEST",
},
500,
url,
undefined,
expect.any(Headers),
),
);
});
@@ -127,6 +135,7 @@ describe("parseErrorResponse", () => {
expect(
parseErrorResponse(
{
url,
headers,
status: 500,
} as Response,
@@ -138,6 +147,9 @@ describe("parseErrorResponse", () => {
errcode: "TEST",
},
500,
url,
undefined,
expect.any(Headers),
),
);
});
@@ -147,8 +159,8 @@ describe("parseErrorResponse", () => {
expect(
parseErrorResponse(
{
responseURL: "https://example.com",
...xhrHeaderMethods,
responseURL: "https://example.com",
status: 500,
} as XMLHttpRequest,
'{"errcode": "TEST"}',
@@ -160,6 +172,8 @@ describe("parseErrorResponse", () => {
},
500,
"https://example.com",
undefined,
expect.any(Headers),
),
);
});
@@ -182,9 +196,40 @@ describe("parseErrorResponse", () => {
},
500,
"https://example.com",
undefined,
expect.any(Headers),
),
);
});
it.each([
{
errcode: MatrixSafetyErrorCode.name,
error: "Spammy",
},
{
errcode: MatrixSafetyErrorCode.name,
error: "Spammy",
expiry: 5000,
},
{
errcode: MatrixSafetyErrorCode.name,
error: "Spammy",
harms: ["m.spam", "org.example.additional-harm"],
expiry: 5000,
},
])("should resolve MatrixSafetyErrors from fetch", (errContent) => {
headers.set("Content-Type", "application/json");
const value = parseErrorResponse(
{
headers,
status: 400,
} as Response,
JSON.stringify(errContent),
) as MatrixSafetyError;
expect(value).toBeInstanceOf(MatrixSafetyError);
expect(value.harms.size).toEqual(errContent.harms?.length ?? 0);
expect(value.expiry?.getTime()).toEqual(errContent.expiry);
});
describe("with HTTP headers", () => {
function addHeaders(headers: Headers) {
@@ -196,7 +241,7 @@ describe("parseErrorResponse", () => {
}
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) {
expect(new Map(otherHeaders)).toEqual(new Map(expectedHeaders));
expect(new Map(otherHeaders as any)).toEqual(new Map(expectedHeaders as any));
}
it("should resolve HTTP Errors from XHR with headers", () => {
@@ -265,7 +310,7 @@ describe("parseErrorResponse", () => {
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(new HTTPError("Server returned 500 error", 500));
).toStrictEqual(new HTTPError("Server returned 500 error", 500, expect.any(Headers)));
});
it("should handle empty type gracefully", () => {
@@ -304,27 +349,27 @@ describe("parseErrorResponse", () => {
} as Response,
"I'm a teapot",
),
).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418));
).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418, expect.any(Headers)));
});
});
describe("retryNetworkOperation", () => {
it("should retry given number of times with exponential sleeps", async () => {
const err = new ConnectionError("test");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
const fn = vi.fn().mockRejectedValue(err);
vi.mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(4);
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
expect(vi.mocked(sleep)).toHaveBeenCalledTimes(3);
expect(vi.mocked(sleep).mock.calls[0][0]).toBe(2000);
expect(vi.mocked(sleep).mock.calls[1][0]).toBe(4000);
expect(vi.mocked(sleep).mock.calls[2][0]).toBe(8000);
});
it("should bail out on errors other than ConnectionError", async () => {
const err = new TypeError("invalid JSON");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
const fn = vi.fn().mockRejectedValue(err);
vi.mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(1);
});
@@ -334,10 +379,10 @@ describe("retryNetworkOperation", () => {
const err2 = new ConnectionError("test2");
const err3 = new ConnectionError("test3");
const errors = [err1, err2, err3];
const fn = jest.fn().mockImplementation(() => {
const fn = vi.fn().mockImplementation(() => {
throw errors.shift();
});
mocked(sleep).mockResolvedValue(undefined);
vi.mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
});
});
+56 -52
View File
@@ -17,7 +17,7 @@ limitations under the License.
import { type MatrixClient } from "../../src/client";
import { logger } from "../../src/logger";
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
import { InteractiveAuth, AuthType, NoAuthFlowFoundError } from "../../src/interactive-auth";
import { HTTPError, MatrixError } from "../../src/http-api";
import { sleep } from "../../src/utils";
import { secureRandomString } from "../../src/randomstring";
@@ -34,14 +34,14 @@ const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixC
describe("InteractiveAuth", () => {
it("should start an auth stage and complete it", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Password] }],
@@ -83,14 +83,14 @@ describe("InteractiveAuth", () => {
});
it("should handle auth errcode presence", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Password] }],
@@ -132,9 +132,9 @@ describe("InteractiveAuth", () => {
});
it("should handle set emailSid for email flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
doRequest,
@@ -186,9 +186,9 @@ describe("InteractiveAuth", () => {
});
it("should make a request if no authdata is provided", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -248,9 +248,9 @@ describe("InteractiveAuth", () => {
});
it("should make a request if authdata is null", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -310,9 +310,9 @@ describe("InteractiveAuth", () => {
});
it("should start an auth stage and reject if no auth flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -337,13 +337,15 @@ describe("InteractiveAuth", () => {
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new NoAuthFlowFoundError("No appropriate authentication flow found", [], []),
);
});
it("should start an auth stage and reject if no auth flow but has session", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -372,13 +374,15 @@ describe("InteractiveAuth", () => {
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new NoAuthFlowFoundError("No appropriate authentication flow found", [], []),
);
});
it("should handle unexpected error types without data property set", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -397,13 +401,13 @@ describe("InteractiveAuth", () => {
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("myerror"));
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new HTTPError("myerror", 401));
});
it("should allow dummy auth", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
@@ -435,9 +439,9 @@ describe("InteractiveAuth", () => {
describe("requestEmailToken", () => {
it("increases auth attempts", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
const ia = new InteractiveAuth({
@@ -464,9 +468,9 @@ describe("InteractiveAuth", () => {
});
it("passes errors through", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
requestEmailToken.mockImplementation(async () => {
throw new Error("unspecific network error");
});
@@ -482,9 +486,9 @@ describe("InteractiveAuth", () => {
});
it("only starts one request at a time", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
const ia = new InteractiveAuth({
@@ -499,9 +503,9 @@ describe("InteractiveAuth", () => {
});
it("stores result in email sid", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const requestEmailToken = vi.fn();
const sid = secureRandomString(24);
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
@@ -518,14 +522,14 @@ describe("InteractiveAuth", () => {
});
it("should prioritise shorter flows", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
@@ -539,14 +543,14 @@ describe("InteractiveAuth", () => {
});
it("should prioritise flows with entirely supported stages", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }],
@@ -561,14 +565,14 @@ describe("InteractiveAuth", () => {
});
it("should fire stateUpdated callback with error when a request fails", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const doRequest = vi.fn();
const stateUpdated = vi.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
requestEmailToken: vi.fn(),
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Password] }],
+1 -1
View File
@@ -23,7 +23,7 @@ let client: MatrixClient;
describe("Local notification settings", () => {
beforeEach(() => {
client = new TestClient("@alice:matrix.org", "123", undefined, undefined, undefined).client;
client.setAccountData = jest.fn();
client.setAccountData = vi.fn();
});
describe("Lets you set local notification settings", () => {
+7 -7
View File
@@ -21,12 +21,12 @@ import loglevel from "loglevel";
import { DebugLogger, logger } from "../../src/logger.ts";
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
describe("logger", () => {
it("should log to console by default", () => {
jest.spyOn(console, "debug").mockReturnValue(undefined);
vi.spyOn(console, "debug").mockReturnValue(undefined);
logger.debug("test1");
logger.log("test2");
@@ -35,8 +35,8 @@ describe("logger", () => {
});
it("should allow creation of child loggers which add a prefix", () => {
jest.spyOn(loglevel, "getLogger");
jest.spyOn(console, "debug").mockReturnValue(undefined);
vi.spyOn(loglevel, "getLogger");
vi.spyOn(console, "debug").mockReturnValue(undefined);
const childLogger = logger.getChild("[prefix1]");
expect(loglevel.getLogger).toHaveBeenCalledWith("matrix-[prefix1]");
@@ -52,7 +52,7 @@ describe("logger", () => {
describe("DebugLogger", () => {
it("should handle empty log messages", () => {
const mockTarget = jest.fn();
const mockTarget = vi.fn();
const logger = new DebugLogger(mockTarget as any);
logger.info();
expect(mockTarget).toHaveBeenCalledTimes(1);
@@ -60,7 +60,7 @@ describe("DebugLogger", () => {
});
it("should handle logging an Error", () => {
const mockTarget = jest.fn();
const mockTarget = vi.fn();
const logger = new DebugLogger(mockTarget as any);
// If there is a stack and a message, we use the stack.
@@ -79,7 +79,7 @@ describe("DebugLogger", () => {
});
it("should handle logging an object", () => {
const mockTarget = jest.fn();
const mockTarget = vi.fn();
const logger = new DebugLogger(mockTarget as any);
const obj = { a: 1 };
+1 -5
View File
@@ -1,4 +1,4 @@
import fetchMock from "fetch-mock-jest";
import fetchMock from "@fetch-mock/vitest";
import { ClientPrefix, MatrixClient } from "../../src";
import { SSOAction } from "../../src/@types/auth";
@@ -68,10 +68,6 @@ describe("SSO login URL", function () {
});
describe("refreshToken", () => {
afterEach(() => {
fetchMock.mockReset();
});
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
const client = createExampleMatrixClient();
File diff suppressed because it is too large Load Diff
+72 -49
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MatrixEvent } from "../../../src";
import { type IContent, type MatrixEvent } from "../../../src";
import {
CallMembership,
type SessionMembershipData,
@@ -25,20 +25,27 @@ import { membershipTemplate } from "./mocks";
function makeMockEvent(originTs = 0): MatrixEvent {
return {
getTs: jest.fn().mockReturnValue(originTs),
getSender: jest.fn().mockReturnValue("@alice:example.org"),
getId: jest.fn().mockReturnValue("$eventid"),
getTs: vi.fn().mockReturnValue(originTs),
getSender: vi.fn().mockReturnValue("@alice:example.org"),
getId: vi.fn().mockReturnValue("$eventid"),
getContent: vi.fn().mockReturnValue({}),
} as unknown as MatrixEvent;
}
function createCallMembership(ev: MatrixEvent, content: IContent): CallMembership {
vi.mocked(ev.getContent).mockReturnValue(content);
const data = CallMembership.membershipDataFromMatrixEvent(ev);
return new CallMembership(ev, data, "xx");
}
describe("CallMembership", () => {
describe("SessionMembershipData", () => {
beforeEach(() => {
jest.useFakeTimers();
vi.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});
const membershipTemplate: SessionMembershipData = {
@@ -53,29 +60,29 @@ describe("CallMembership", () => {
it("rejects membership with no device_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
}).toThrow();
});
it("rejects membership with no call_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
}).toThrow();
});
it("allow membership with no scope", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
}).not.toThrow();
});
it("uses event timestamp if no created_ts", () => {
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
const membership = createCallMembership(makeMockEvent(12345), membershipTemplate);
expect(membership.createdTs()).toEqual(12345);
});
it("uses created_ts if present", () => {
const membership = new CallMembership(
const membership = createCallMembership(
makeMockEvent(12345),
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
);
@@ -84,28 +91,28 @@ describe("CallMembership", () => {
it("considers memberships unexpired if local age low enough", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
});
it("considers memberships expired if local age large enough", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
});
it("returns preferred foci", () => {
const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" };
const membership = new CallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
const membership = createCallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
expect(membership.transports).toEqual([mockFocus]);
});
describe("getTransport", () => {
const mockFocus = { type: "this_is_a_mock_focus" };
const oldestMembership = new CallMembership(makeMockEvent(), membershipTemplate);
const oldestMembership = createCallMembership(makeMockEvent(), membershipTemplate);
it("gets the correct active transport with oldest_membership", () => {
const membership = new CallMembership(makeMockEvent(), {
const membership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
@@ -119,7 +126,7 @@ describe("CallMembership", () => {
});
it("gets the correct active transport with multi_sfu", () => {
const membership = new CallMembership(makeMockEvent(), {
const membership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "multi_sfu" },
@@ -132,7 +139,7 @@ describe("CallMembership", () => {
expect(membership.getTransport(oldestMembership)).toBe(mockFocus);
});
it("does not provide focus if the selection method is unknown", () => {
const membership = new CallMembership(makeMockEvent(), {
const membership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "unknown" },
@@ -143,7 +150,7 @@ describe("CallMembership", () => {
});
});
describe("correct values from computed fields", () => {
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org");
});
@@ -170,7 +177,7 @@ describe("CallMembership", () => {
expect(membership.scope).toBe("m.room");
});
it("returns correct membershipID", () => {
expect(membership.membershipID).toBe("0");
expect(membership.membershipID).toBe("@alice:example.org:AAAAAAA");
});
it("returns correct unused fields", () => {
expect(membership.getAbsoluteExpiry()).toBe(DEFAULT_EXPIRE_DURATION);
@@ -192,29 +199,29 @@ describe("CallMembership", () => {
it("rejects membership with no slot_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
}).toThrow();
});
it("rejects membership with invalid slot_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "invalid_slot_id" });
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "invalid_slot_id" });
}).toThrow();
});
it("accepts membership with valid slot_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#" });
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#" });
}).not.toThrow();
});
it("rejects membership with no application", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
createCallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
}).toThrow();
});
it("rejects membership with incorrect application", () => {
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
application: { wrong_type_key: "unknown" },
});
@@ -223,34 +230,34 @@ describe("CallMembership", () => {
it("rejects membership with no member", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
}).toThrow();
});
it("rejects membership with incorrect member", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id_wrong: "test" },
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" },
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id: "@@test" },
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id: "@test-wrong-user:user.id" },
});
@@ -258,41 +265,41 @@ describe("CallMembership", () => {
});
it("rejects membership with incorrect sticky_key", () => {
expect(() => {
new CallMembership(makeMockEvent(), membershipTemplate);
createCallMembership(makeMockEvent(), membershipTemplate);
}).not.toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
sticky_key: 1,
msc4354_sticky_key: undefined,
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
sticky_key: "1",
msc4354_sticky_key: undefined,
});
}).not.toThrow();
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, msc4354_sticky_key: undefined });
createCallMembership(makeMockEvent(), { ...membershipTemplate, msc4354_sticky_key: undefined });
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
msc4354_sticky_key: 1,
sticky_key: "valid",
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
msc4354_sticky_key: "valid",
sticky_key: "valid",
});
}).not.toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
createCallMembership(makeMockEvent(), {
...membershipTemplate,
msc4354_sticky_key: "valid_but_different",
sticky_key: "valid",
@@ -300,21 +307,21 @@ describe("CallMembership", () => {
}).toThrow();
});
it("considers memberships unexpired if local age low enough", () => {
it.skip("considers memberships unexpired if local age low enough", () => {
// TODO link prev event
});
it("considers memberships expired if local age large enough", () => {
it.skip("considers memberships expired if local age large enough", () => {
// TODO link prev event
});
describe("getTransport", () => {
it("gets the correct active transport with oldest_membership", () => {
const oldestMembership = new CallMembership(makeMockEvent(), {
const oldestMembership = createCallMembership(makeMockEvent(), {
...membershipTemplate,
rtc_transports: [{ type: "oldest_transport" }],
});
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });
@@ -324,7 +331,7 @@ describe("CallMembership", () => {
});
});
describe("correct values from computed fields", () => {
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org");
});
@@ -372,19 +379,35 @@ describe("CallMembership", () => {
beforeEach(() => {
// server origin timestamp for this event is 1000
fakeEvent = makeMockEvent(1000);
membership = new CallMembership(fakeEvent!, membershipTemplate);
membership = createCallMembership(fakeEvent!, membershipTemplate);
jest.useFakeTimers();
vi.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
});
it("calculates time until expiry", () => {
jest.setSystemTime(2000);
vi.setSystemTime(2000);
// should be using absolute expiry time
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
});
});
it("uses unpadded base64 for RTC backend identities", async () => {
expect(
await CallMembership.computeRtcBackendIdentity(makeMockEvent(), {
kind: "rtc",
data: {
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: "xyzRANDOMxyz" },
rtc_transports: [{ type: "livekit" }],
versions: [],
msc4354_sticky_key: "abc123",
},
}),
).toBe("2+h2ELE1XY/NsuveToZOekORCoyQMO6V0W7XZUWk5Q4");
});
});
File diff suppressed because it is too large Load Diff
@@ -14,136 +14,149 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks";
import { ClientEvent, EventTimeline, MatrixClient, type Room, RoomStateEvent } from "../../../src";
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc";
import { makeMockRoom, type MembershipData, membershipTemplate, mockRoomState, mockRTCEvent } from "./mocks";
import { logger } from "../../../src/logger";
import { flushPromises } from "../../test-utils/flushPromises";
describe("MatrixRTCSessionManager", () => {
let client: MatrixClient;
describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
"MatrixRTCSessionManager ($eventKind)",
({ eventKind }) => {
let client: MatrixClient;
beforeEach(() => {
client = new MatrixClient({ baseUrl: "base_url" });
client.matrixRTC.start();
});
async function sendLeaveMembership(room: Room, membershipData: MembershipData[]): Promise<void> {
if (eventKind === "memberState") {
mockRoomState(room, [{ user_id: membershipTemplate.user_id }]);
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
} else {
membershipData.splice(0, 1, { user_id: membershipTemplate.user_id });
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
}
await flushPromises();
}
afterEach(() => {
client.stopClient();
client.matrixRTC.stop();
});
beforeEach(() => {
client = new MatrixClient({ baseUrl: "base_url" });
client.matrixRTC.start();
});
it("Fires event when session starts", () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
afterEach(() => {
client.stopClient();
client.matrixRTC.stop();
});
try {
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
it("Fires event when session starts", async () => {
const onStarted = vi.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
try {
const room1 = makeMockRoom([membershipTemplate], eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
await flushPromises();
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
}
});
it("Doesn't fire event if unrelated sessions starts", () => {
const onStarted = vi.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "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 onEnded = vi.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const membershipData: MembershipData[] = [membershipTemplate];
const room1 = makeMockRoom(membershipData, eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
vi.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
await flushPromises();
await sendLeaveMembership(room1, membershipData);
expect(onEnded).toHaveBeenCalledWith(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);
try {
// Create a session for applicaation m.other, we ignore this session ecause it lacks a call_id
const room1MembershipData: MembershipData[] = [{ ...membershipTemplate, application: "m.other" }];
const room1 = makeMockRoom(room1MembershipData, eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
await flushPromises();
expect(onStarted).not.toHaveBeenCalled();
onStarted.mockClear();
// Create a session for applicaation m.notCall. We expect this call to be tracked because it has a call_id
const room2MembershipData: MembershipData[] = [
{ ...membershipTemplate, application: "m.notCall", call_id: "test" },
];
const room2 = makeMockRoom(room2MembershipData, eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
client.emit(ClientEvent.Room, room2);
await flushPromises();
expect(onStarted).toHaveBeenCalled();
onStarted.mockClear();
// Stop room1's RTC session. Tracked.
vi.spyOn(client, "getRoom").mockReturnValue(room2);
await sendLeaveMembership(room2, room2MembershipData);
expect(onEnded).toHaveBeenCalled();
onEnded.mockClear();
// Stop room1's RTC session. Not tracked.
vi.spyOn(client, "getRoom").mockReturnValue(room1);
await sendLeaveMembership(room1, room1MembershipData);
expect(onEnded).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
}
});
it("Doesn't fire event if unrelated sessions ends", async () => {
const onEnded = vi.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const membership: MembershipData[] = [{ ...membershipTemplate, application: "m.other_app" }];
const room1 = makeMockRoom(membership, eventKind === "sticky");
vi.spyOn(client, "getRooms").mockReturnValue([room1]);
vi.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
}
});
it("Doesn't fire event if unrelated sessions starts", () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
await sendLeaveMembership(room1, membership);
try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
jest.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", () => {
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);
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
it("Fires correctly with for with custom sessionDescription", () => {
const onStarted = jest.fn();
const onEnded = jest.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);
try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
onStarted.mockClear();
const room2 = makeMockRoom([{ ...membershipTemplate, application: "m.notCall", call_id: "test" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
client.emit(ClientEvent.Room, room2);
expect(onStarted).toHaveBeenCalled();
onStarted.mockClear();
mockRoomState(room2, [{ user_id: membershipTemplate.user_id }]);
jest.spyOn(client, "getRoom").mockReturnValue(room2);
const roomState = room2.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
expect(onEnded).toHaveBeenCalled();
onEnded.mockClear();
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
const roomStateOther = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEventOther = roomStateOther.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEventOther, roomStateOther, null);
expect(onEnded).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
}
});
it("Doesn't fire event if unrelated sessions ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other_app" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
});
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
},
);
+200 -149
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MockedFunction, type Mock } from "jest-mock";
import { type Mock, type MockedFunction } from "vitest";
import {
type EmptyObject,
@@ -23,6 +23,7 @@ import {
MatrixError,
UnsupportedDelayedEventsEndpointError,
type Room,
MAX_STICKY_DURATION_MS,
} from "../../../src";
import {
MembershipManagerEvent,
@@ -32,7 +33,7 @@ import {
type LivekitFocusSelection,
} from "../../../src/matrixrtc";
import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
import { MembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
import { MembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
/**
* Create a promise that will resolve once a mocked method is called.
@@ -40,7 +41,7 @@ import { MembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
* @param returnVal Provide an optional value that the mocked method should return. (use Promise.resolve(val) or Promise.reject(err))
* @returns The promise that resolves once the method is called.
*/
function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>): Promise<void> {
function waitForMockCall(method: MockedFunction<(...args: any[]) => any>, returnVal?: Promise<any>): Promise<void> {
const { promise, resolve } = Promise.withResolvers<void>();
method.mockImplementation(() => {
resolve();
@@ -50,7 +51,7 @@ function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>):
}
/** See waitForMockCall */
function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<any>) {
function waitForMockCallOnce(method: MockedFunction<(...args: any[]) => any>, returnVal?: Promise<any>) {
const { promise, resolve } = Promise.withResolvers<void>();
method.mockImplementationOnce(() => {
resolve();
@@ -64,7 +65,7 @@ function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<an
* @param method The method to control the resolve timing.
* @returns
*/
function createAsyncHandle<T>(method: MockedFunction<any>) {
function createAsyncHandle<T>(method: MockedFunction<(...args: any[]) => any>) {
const { reject, resolve, promise } = Promise.withResolvers<T>();
method.mockImplementation(() => promise);
return { reject, resolve };
@@ -87,17 +88,22 @@ describe("MembershipManager", () => {
beforeEach(() => {
// Default to fake timers.
jest.useFakeTimers();
vi.useFakeTimers();
client = makeMockClient("@alice:example.org", "AAAAAAA");
room = makeMockRoom([membershipTemplate]);
// Provide a default mock that is like the default "non error" server behaviour.
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client.sendStateEvent as Mock<any>).mockResolvedValue(undefined);
vi.mocked(client._unstable_sendDelayedStateEvent).mockResolvedValue({ delay_id: "id" });
vi.mocked(client._unstable_updateDelayedEvent).mockResolvedValue({});
vi.mocked(client._unstable_cancelScheduledDelayedEvent).mockResolvedValue({});
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockResolvedValue({});
vi.mocked(client._unstable_sendScheduledDelayedEvent).mockResolvedValue({});
vi.mocked(client._unstable_sendStickyEvent).mockResolvedValue({ event_id: "id" });
vi.mocked(client._unstable_sendStickyDelayedEvent).mockResolvedValue({ delay_id: "id" });
vi.mocked(client.sendStateEvent).mockResolvedValue({ event_id: "id" });
});
afterEach(() => {
jest.useRealTimers();
vi.useRealTimers();
// There is no need to clean up mocks since we will recreate the client.
});
@@ -119,7 +125,9 @@ describe("MembershipManager", () => {
it("sends a membership event and schedules delayed leave when joining a call", async () => {
// Spys/Mocks
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
client._unstable_restartScheduledDelayedEvent,
);
// Test
const memberManager = new MembershipManager(undefined, room, client, callSession);
@@ -135,12 +143,13 @@ describe("MembershipManager", () => {
device_id: "AAAAAAA",
expires: 14400000,
foci_preferred: [focus],
membershipID: "@alice:example.org:AAAAAAA",
focus_active: focusActive,
scope: "m.room",
},
"_@alice:example.org_AAAAAAA_m.call",
);
updateDelayedEventHandle.resolve?.();
restartScheduledDelayedEventHandle.resolve?.();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
room.roomId,
{ delay: 8000 },
@@ -151,54 +160,17 @@ describe("MembershipManager", () => {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("sends a rtc membership event when using `useRtcMemberFormat`", async () => {
// Spys/Mocks
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
// Test
const memberManager = new MembershipManager({ useRtcMemberFormat: true }, room, client, callSession);
memberManager.join([], focus);
// expects
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
expect(client.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
"org.matrix.msc4143.rtc.member",
{
application: { type: "m.call" },
member: {
user_id: "@alice:example.org",
id: "_@alice:example.org_AAAAAAA_m.call",
device_id: "AAAAAAA",
},
slot_id: "m.call#",
rtc_transports: [focus],
versions: [],
},
"_@alice:example.org_AAAAAAA_m.call",
);
updateDelayedEventHandle.resolve?.();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
room.roomId,
{ delay: 8000 },
"org.matrix.msc4143.rtc.member",
{},
"_@alice:example.org_AAAAAAA_m.call",
);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("reschedules delayed leave event if sending state cancels it", async () => {
const memberManager = new MembershipManager(undefined, room, client, callSession);
const waitForSendState = waitForMockCall(client.sendStateEvent);
const waitForUpdateDelaye = waitForMockCallOnce(
client._unstable_updateDelayedEvent,
const waitForRestartScheduledDelayedEvent = waitForMockCallOnce(
client._unstable_restartScheduledDelayedEvent,
Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
);
memberManager.join([focus], focusActive);
await waitForSendState;
await waitForUpdateDelaye;
await jest.advanceTimersByTimeAsync(1);
await waitForRestartScheduledDelayedEvent;
await vi.advanceTimersByTimeAsync(1);
// Once for the initial event and once because of the errcode: "M_NOT_FOUND"
// Different to "sends a membership event and schedules delayed leave when joining a call" where its only called once (1)
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
@@ -211,9 +183,9 @@ describe("MembershipManager", () => {
// - run into rate limit for sending delayed event
// - run into rate limit when setting membership state.
if (useOwnedStateEvents) {
room.getVersion = jest.fn().mockReturnValue("org.matrix.msc3757.default");
room.getVersion = vi.fn().mockReturnValue("org.matrix.msc3757.default");
}
const updatedDelayedEvent = waitForMockCall(client._unstable_updateDelayedEvent);
const restartScheduledDelayedEvent = waitForMockCall(client._unstable_restartScheduledDelayedEvent);
const sentDelayedState = waitForMockCall(
client._unstable_sendDelayedStateEvent,
Promise.resolve({
@@ -228,7 +200,7 @@ describe("MembershipManager", () => {
"org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
"org.matrix.msc4140.max_delay": 7500,
});
(client._unstable_sendDelayedStateEvent as Mock).mockImplementationOnce(() => {
vi.mocked(client._unstable_sendDelayedStateEvent).mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
@@ -238,7 +210,7 @@ describe("MembershipManager", () => {
// preparing the delayed disconnect should handle ratelimiting
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
(client._unstable_sendDelayedStateEvent as Mock).mockImplementationOnce(() => {
vi.mocked(client._unstable_sendDelayedStateEvent).mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
@@ -253,7 +225,7 @@ describe("MembershipManager", () => {
undefined,
new Headers({ "Retry-After": "1" }),
);
(client.sendStateEvent as Mock).mockImplementationOnce(() => {
vi.mocked(client.sendStateEvent).mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
@@ -276,11 +248,11 @@ describe("MembershipManager", () => {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(1, ...callProps(9000));
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(2, ...callProps(7500));
await jest.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(5000);
await sendStateEventAttempt.then(); // needed to resolve after resendIfRateLimited catches
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
expect(client.sendStateEvent).toHaveBeenCalledWith(
room!.roomId,
@@ -292,6 +264,7 @@ describe("MembershipManager", () => {
expires: 14400000,
device_id: "AAAAAAA",
foci_preferred: [focus],
membershipID: "@alice:example.org:AAAAAAA",
focus_active: focusActive,
} satisfies SessionMembershipData,
userStateKey,
@@ -299,19 +272,21 @@ describe("MembershipManager", () => {
await sentDelayedState;
// should have prepared the heartbeat to keep delaying the leave event while still connected
await updatedDelayedEvent;
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
await restartScheduledDelayedEvent;
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
// ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers.
await jest.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(5000);
// should update delayed disconnect
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
}
// eslint-disable-next-line @vitest/expect-expect
it("sends a membership event after rate limits during delayed event setup when joining a call", async () => {
await testJoin(false);
});
// eslint-disable-next-line @vitest/expect-expect
it("does not prefix the state key with _ for rooms that support user-owned state events", async () => {
await testJoin(true);
});
@@ -320,7 +295,7 @@ describe("MembershipManager", () => {
describe("delayed leave event", () => {
it("does not try again to schedule a delayed leave event if not supported", () => {
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
delayedHandle.reject?.(
@@ -332,11 +307,11 @@ describe("MembershipManager", () => {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("does try to schedule a delayed leave event again if rate limited", async () => {
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
delayedHandle.reject?.(new HTTPError("rate limited", 429, undefined));
await jest.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(5000);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
});
it("uses delayedLeaveEventDelayMs from config", () => {
@@ -365,7 +340,7 @@ describe("MembershipManager", () => {
manager.join([focus]);
expect(manager.status).toBe(Status.Connecting);
// Let the scheduler run one iteration so that we can send the join state event
await jest.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(manager.status).toBe(Status.Connected);
// Now that we are connected, we set up the mocks.
@@ -377,17 +352,17 @@ describe("MembershipManager", () => {
// (onRTCSessionMemberUpdate)
// - Only then do we resolve the sending of the delayed event.
// - We test that the manager acknowledges the leave and sends a new membership state event.
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValueOnce(
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValueOnce(
new MatrixError({ errcode: "M_NOT_FOUND" }),
);
const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent);
await jest.advanceTimersByTimeAsync(RESTART_DELAY);
await vi.advanceTimersByTimeAsync(RESTART_DELAY);
// first simulate the sync, then resolve sending the delayed event.
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
resolve({ delay_id: "id" });
// Let the scheduler run one iteration so that the new join gets sent
await jest.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
});
@@ -412,6 +387,7 @@ describe("MembershipManager", () => {
device_id: "AAAAAAA",
expires: 1234567,
foci_preferred: [focus],
membershipID: "@alice:example.org:AAAAAAA",
focus_active: {
focus_selection: "oldest_membership",
type: "livekit",
@@ -436,19 +412,19 @@ describe("MembershipManager", () => {
it("resolves delayed leave event when leave is called", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
await manager.leave();
expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", "send");
expect(client._unstable_sendScheduledDelayedEvent).toHaveBeenLastCalledWith("id");
expect(client.sendStateEvent).toHaveBeenCalled();
});
it("send leave event when leave is called and resolving delayed leave fails", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus]);
await jest.advanceTimersByTimeAsync(1);
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown");
await vi.advanceTimersByTimeAsync(1);
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue("unknown");
await manager.leave();
// We send a normal leave event since we failed using updateDelayedEvent with the "send" action.
// We send a normal leave event since we failed using sendScheduledDelayedEvent.
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
room.roomId,
"org.matrix.msc3401.call.member",
@@ -456,9 +432,9 @@ describe("MembershipManager", () => {
"_@alice:example.org_AAAAAAA_m.call",
);
});
it("does nothing if not joined", () => {
it("does nothing if not joined", async () => {
const manager = new MembershipManager({}, room, client, callSession);
expect(async () => await manager.leave()).not.toThrow();
await expect(manager.leave()).resolves.toBeTruthy();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client.sendStateEvent).not.toHaveBeenCalled();
});
@@ -468,20 +444,26 @@ describe("MembershipManager", () => {
it("does nothing if not joined", async () => {
const manager = new MembershipManager({}, room, client, callSession);
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await jest.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_cancelScheduledDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_restartScheduledDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_sendScheduledDelayedEvent).not.toHaveBeenCalled();
});
it("does nothing if own membership still present", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
const myMembership = (client.sendStateEvent as Mock).mock.calls[0][2];
await vi.advanceTimersByTimeAsync(1);
const myMembership = vi.mocked(client.sendStateEvent).mock.calls[0][2];
// reset all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
(client.sendStateEvent as Mock).mockClear();
(client._unstable_updateDelayedEvent as Mock).mockClear();
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
vi.mocked(client.sendStateEvent).mockClear();
vi.mocked(client._unstable_updateDelayedEvent).mockClear();
vi.mocked(client._unstable_cancelScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_sendScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_sendDelayedStateEvent).mockClear();
await manager.onRTCSessionMemberUpdate([
mockCallMembership(membershipTemplate, room.roomId),
@@ -491,53 +473,56 @@ describe("MembershipManager", () => {
),
]);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client.sendStateEvent).not.toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_cancelScheduledDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_restartScheduledDelayedEvent).not.toHaveBeenCalled();
expect(client._unstable_sendScheduledDelayedEvent).not.toHaveBeenCalled();
});
it("recreates membership if it is missing", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
(client.sendStateEvent as Mock).mockClear();
(client._unstable_updateDelayedEvent as Mock).mockClear();
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
vi.mocked(client.sendStateEvent).mockClear();
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_sendDelayedStateEvent).mockClear();
// Our own membership is removed:
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client.sendStateEvent).toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
expect(client._unstable_updateDelayedEvent).toHaveBeenCalled();
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalled();
});
it("updates the UpdateExpiry entry in the action scheduler", async () => {
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
(client.sendStateEvent as Mock).mockClear();
(client._unstable_updateDelayedEvent as Mock).mockClear();
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
vi.mocked(client.sendStateEvent).mockClear();
vi.mocked(client._unstable_restartScheduledDelayedEvent).mockClear();
vi.mocked(client._unstable_sendDelayedStateEvent).mockClear();
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValueOnce(
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValueOnce(
new MatrixError({ errcode: "M_NOT_FOUND" }),
);
const { resolve } = createAsyncHandle(client._unstable_sendDelayedStateEvent);
await jest.advanceTimersByTimeAsync(10_000);
await vi.advanceTimersByTimeAsync(10_000);
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
resolve({ delay_id: "id" });
await jest.advanceTimersByTimeAsync(10_000);
await vi.advanceTimersByTimeAsync(10_000);
expect(client.sendStateEvent).toHaveBeenCalled();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
expect(client._unstable_updateDelayedEvent).toHaveBeenCalled();
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalled();
expect(manager.status).toBe(Status.Connected);
});
});
@@ -552,22 +537,22 @@ describe("MembershipManager", () => {
{ id: "", application: "m.call" },
);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
// The first call is from checking id the server deleted the delayed event
// so it does not need a `advanceTimersByTime`
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
// TODO: Check that update delayed event is called with the correct HTTP request timeout
// expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
// expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
for (let i = 2; i <= 12; i++) {
// flush promises before advancing the timers to make sure schedulers are setup
await jest.advanceTimersByTimeAsync(10_000);
await vi.advanceTimersByTimeAsync(10_000);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(i);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(i);
// TODO: Check that update delayed event is called with the correct HTTP request timeout
// expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
// expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
}
});
@@ -586,18 +571,20 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive);
await waitForMockCall(client.sendStateEvent);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
const sentMembership = (client.sendStateEvent as Mock).mock.calls[0][2] as SessionMembershipData;
const sentMembership = vi.mocked(client.sendStateEvent).mock.calls[0][2] as SessionMembershipData;
expect(sentMembership.expires).toBe(expire);
for (let i = 2; i <= 12; i++) {
await jest.advanceTimersByTimeAsync(expire);
await vi.advanceTimersByTimeAsync(expire);
expect(client.sendStateEvent).toHaveBeenCalledTimes(i);
const sentMembership = (client.sendStateEvent as Mock).mock.lastCall![2] as SessionMembershipData;
const sentMembership = vi.mocked(client.sendStateEvent).mock.lastCall![2] as SessionMembershipData;
expect(sentMembership.expires).toBe(expire * i);
}
}
// eslint-disable-next-line @vitest/expect-expect
it("extends `expires` when call still active", async () => {
await testExpires(10_000);
});
// eslint-disable-next-line @vitest/expect-expect
it("extends `expires` using headroom configuration", async () => {
await testExpires(10_000, 1_000);
});
@@ -614,23 +601,23 @@ describe("MembershipManager", () => {
const manager = new MembershipManager({}, room, client, callSession);
expect(manager.status).toBe(Status.Disconnected);
const connectEmit = jest.fn();
const connectEmit = vi.fn();
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
manager.join([focus], focusActive);
expect(manager.status).toBe(Status.Connecting);
handleDelayedEvent.resolve();
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(connectEmit).toHaveBeenCalledWith(Status.Disconnected, Status.Connecting);
handleStateEvent.resolve();
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected);
});
it("emits 'Disconnecting' and 'Disconnected' after leave", async () => {
const manager = new MembershipManager({}, room, client, callSession);
const connectEmit = jest.fn();
const connectEmit = vi.fn();
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
await manager.leave();
expect(connectEmit).toHaveBeenCalledWith(Status.Connected, Status.Disconnecting);
expect(connectEmit).toHaveBeenCalledWith(Status.Disconnecting, Status.Disconnected);
@@ -655,7 +642,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "1" }),
),
);
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
});
@@ -673,7 +660,7 @@ describe("MembershipManager", () => {
// Should call _unstable_sendDelayedStateEvent but not sendStateEvent because of the
// RateLimit error.
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
@@ -681,7 +668,7 @@ describe("MembershipManager", () => {
// the membership is no longer present on the homeserver
await manager.onRTCSessionMemberUpdate([]);
// Wait for all timers to be setup
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
// We should send the first own membership and a new delayed event after the rate limit timeout.
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
@@ -701,13 +688,13 @@ describe("MembershipManager", () => {
),
);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
// the user terminated the call locally
await manager.leave();
// Wait for all timers to be setup
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
// No new events should have been sent:
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
@@ -715,7 +702,7 @@ describe("MembershipManager", () => {
});
describe("retries sending update delayed leave event restart", () => {
it("resends the initial check delayed update event", async () => {
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue(
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
429,
@@ -728,18 +715,18 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive);
// Hit rate limit
await jest.advanceTimersByTimeAsync(1);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
// Hit second rate limit.
await jest.advanceTimersByTimeAsync(1000);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(1000);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
// Setup resolve
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
await jest.advanceTimersByTimeAsync(1000);
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
await vi.advanceTimersByTimeAsync(1000);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
});
});
@@ -747,7 +734,7 @@ describe("MembershipManager", () => {
describe("unrecoverable errors", () => {
// because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
it("throws, when reaching maximum number of retries for initial delayed event creation", async () => {
const delayEventSendError = jest.fn();
const delayEventSendError = vi.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
@@ -761,14 +748,14 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive, delayEventSendError);
for (let i = 0; i < 10; i++) {
await jest.advanceTimersByTimeAsync(2000);
await vi.advanceTimersByTimeAsync(2000);
}
expect(delayEventSendError).toHaveBeenCalled();
});
// because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
it("throws, when reaching maximum number of retries", async () => {
const delayEventRestartError = jest.fn();
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue(
const delayEventRestartError = vi.fn();
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
429,
@@ -781,12 +768,12 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive, delayEventRestartError);
for (let i = 0; i < 10; i++) {
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
}
expect(delayEventRestartError).toHaveBeenCalled();
});
it("falls back to using pure state events when some error occurs while sending delayed events", async () => {
const unrecoverableError = jest.fn();
const unrecoverableError = vi.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601));
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive, unrecoverableError);
@@ -795,7 +782,7 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).toHaveBeenCalled();
});
it("retries before failing in case its a network error", async () => {
const unrecoverableError = jest.fn();
const unrecoverableError = vi.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 501));
const manager = new MembershipManager(
{ networkErrorRetryMs: 1000, maximumNetworkErrorRetryCount: 7 },
@@ -806,7 +793,7 @@ describe("MembershipManager", () => {
manager.join([focus], focusActive, unrecoverableError);
for (let retries = 0; retries < 7; retries++) {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(retries + 1);
await jest.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1000);
}
expect(unrecoverableError).toHaveBeenCalled();
expect(unrecoverableError.mock.lastCall![0].message).toMatch(
@@ -815,13 +802,13 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).not.toHaveBeenCalled();
});
it("falls back to using pure state events when UnsupportedDelayedEventsEndpointError encountered for delayed events", async () => {
const unrecoverableError = jest.fn();
const unrecoverableError = vi.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
new UnsupportedDelayedEventsEndpointError("not supported", "sendDelayedStateEvent"),
);
const manager = new MembershipManager({}, room, client, callSession);
manager.join([focus], focusActive, unrecoverableError);
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
expect(unrecoverableError).not.toHaveBeenCalled();
expect(client.sendStateEvent).toHaveBeenCalled();
@@ -837,43 +824,43 @@ describe("MembershipManager", () => {
callSession,
);
const { promise: stuckPromise, reject: rejectStuckPromise } = Promise.withResolvers<EmptyObject>();
const probablyLeftEmit = jest.fn();
const probablyLeftEmit = vi.fn();
manager.on(MembershipManagerEvent.ProbablyLeft, probablyLeftEmit);
manager.join([focus], focusActive);
try {
// Let the scheduler run one iteration so that we can send the join state event
await waitForMockCall(client._unstable_updateDelayedEvent);
await waitForMockCall(client._unstable_restartScheduledDelayedEvent);
// We never resolve the delayed event so that we can test the probablyLeft event.
// This simulates the case where the server does not respond to the delayed event.
client._unstable_updateDelayedEvent = jest.fn(() => stuckPromise);
client._unstable_restartScheduledDelayedEvent = vi.fn((_) => stuckPromise);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(manager.status).toBe(Status.Connected);
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
// We expect the probablyLeft event to be emitted after the `delayedLeaveEventDelayMs` = 10000.
// We also track the calls to updated the delayed event that all will never resolve to simulate the server not responding.
// The numbers are a bit arbitrary since we use the local timeout that does not perfectly match the 5s check interval in this test.
await jest.advanceTimersByTimeAsync(5000);
await vi.advanceTimersByTimeAsync(5000);
// No emission after 5s
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(4999);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3);
await vi.advanceTimersByTimeAsync(4999);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
// Reset mocks before we setup the next delayed event restart by advancing the timers 1 more ms.
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue({});
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue({});
// Emit after 10s
await jest.advanceTimersByTimeAsync(1);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(4);
await vi.advanceTimersByTimeAsync(1);
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(4);
expect(probablyLeftEmit).toHaveBeenCalledWith(true);
// Mock a sync which does not include our own membership
await manager.onRTCSessionMemberUpdate([]);
// Wait for the current ongoing delayed event sending to finish
await jest.advanceTimersByTimeAsync(1);
await vi.advanceTimersByTimeAsync(1);
// We should send a new state event and an associated delayed leave event.
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
@@ -886,6 +873,7 @@ describe("MembershipManager", () => {
});
describe("updateCallIntent()", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should fail if the user has not joined the call", async () => {
const manager = new MembershipManager({}, room, client, callSession);
// After joining we want our own focus to be the one we select.
@@ -903,7 +891,7 @@ describe("MembershipManager", () => {
await manager.onRTCSessionMemberUpdate([membership]);
await manager.updateCallIntent("video");
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
const eventContent = (client.sendStateEvent as Mock).mock.calls[0][2] as SessionMembershipData;
const eventContent = vi.mocked(client.sendStateEvent).mock.calls[0][2] as SessionMembershipData;
expect(eventContent["created_ts"]).toEqual(membership.createdTs());
expect(eventContent["m.call.intent"]).toEqual("video");
});
@@ -921,6 +909,69 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).toHaveBeenCalledTimes(0);
});
});
describe("StickyEventMembershipManager", () => {
beforeEach(() => {
// Provide a default mock that is like the default "non error" server behaviour.
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue(undefined);
});
describe("join()", () => {
describe("sends an rtc membership event", () => {
it("sends a membership event and schedules delayed leave when joining a call", async () => {
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
client._unstable_restartScheduledDelayedEvent,
);
const memberManager = new StickyEventMembershipManager(
undefined,
room,
client,
callSession,
"@alice:example.org:AAAAAAA_m.call",
);
memberManager.join([], focus);
await waitForMockCall(client._unstable_sendStickyEvent, Promise.resolve({ event_id: "id" }));
// Test we sent the initial join
expect(client._unstable_sendStickyEvent).toHaveBeenCalledWith(
room.roomId,
3600000,
null,
"org.matrix.msc4143.rtc.member",
{
application: { type: "m.call" },
member: {
user_id: "@alice:example.org",
id: "@alice:example.org:AAAAAAA_m.call",
device_id: "AAAAAAA",
},
slot_id: "m.call#",
rtc_transports: [focus],
versions: [],
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
},
);
restartScheduledDelayedEventHandle.resolve?.();
// Ensure we have sent the delayed disconnect event.
expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledWith(
room.roomId,
MAX_STICKY_DURATION_MS,
{ delay: 8000 },
null,
"org.matrix.msc4143.rtc.member",
{
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
},
);
// ..once
expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledTimes(1);
});
});
});
});
});
it("Should prefix log with MembershipManager used", () => {
@@ -929,7 +980,7 @@ it("Should prefix log with MembershipManager used", () => {
const membershipManager = new MembershipManager(undefined, room, client, callSession);
const spy = jest.spyOn(console, "error");
const spy = vi.spyOn(console, "error");
// Double join
membershipManager.join([]);
membershipManager.join([]);
@@ -25,9 +25,9 @@ describe("OutdatedKeyFilter Test", () => {
const olderKey = fakeInboundSessionWithTimestamp(300);
// Simulate receiving out of order keys
expect(filter.isOutdated(aKey.participantId, aKey)).toBe(false);
expect(filter.isOutdated(aKey.membership, aKey)).toBe(false);
// Then we receive the most recent key out of order
const isOutdated = filter.isOutdated(aKey.participantId, olderKey);
const isOutdated = filter.isOutdated(aKey.membership, olderKey);
// this key is older and should be ignored even if received after
expect(isOutdated).toBe(true);
});
@@ -36,7 +36,7 @@ describe("OutdatedKeyFilter Test", () => {
return {
keyIndex: 0,
creationTS: ts,
participantId: "@alice:localhost|ABCDE",
membership: { userId: "@alice:localhost", deviceId: "ABDE", memberId: "@alice:localhost:ABCDE" },
key: new Uint8Array(16),
};
}
File diff suppressed because it is too large Load Diff
@@ -1,187 +0,0 @@
/*
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 { type Mocked } from "jest-mock";
import { makeKey, makeMockEvent, makeMockRoom } from "./mocks";
import { EventType, type IRoomTimelineData, type Room, RoomEvent, type MatrixClient } from "../../../src";
import { ToDeviceKeyTransport } from "../../../src/matrixrtc/ToDeviceKeyTransport.ts";
import {
getMockClientWithEventEmitter,
mockClientMethodsEvents,
mockClientMethodsUser,
} from "../../test-utils/client.ts";
import { type ParticipantDeviceInfo, type Statistics } from "../../../src/matrixrtc";
import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport.ts";
import { type Logger } from "../../../src/logger.ts";
import { RoomAndToDeviceEvents, RoomAndToDeviceTransport } from "../../../src/matrixrtc/RoomAndToDeviceKeyTransport.ts";
import { RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport.ts";
describe("RoomAndToDeviceTransport", () => {
const roomId = "!room:id";
let mockClient: Mocked<MatrixClient>;
let statistics: Statistics;
let mockLogger: Mocked<Logger>;
let transport: RoomAndToDeviceTransport;
let mockRoom: Room;
let sendEventMock: jest.Mock;
let roomKeyTransport: RoomKeyTransport;
let toDeviceKeyTransport: ToDeviceKeyTransport;
let toDeviceSendKeySpy: jest.SpyInstance;
let roomSendKeySpy: jest.SpyInstance;
beforeEach(() => {
sendEventMock = jest.fn();
mockClient = getMockClientWithEventEmitter({
encryptAndSendToDevice: jest.fn(),
getDeviceId: jest.fn().mockReturnValue("MYDEVICE"),
...mockClientMethodsEvents(),
...mockClientMethodsUser("@alice:example.org"),
sendEvent: sendEventMock,
});
mockRoom = makeMockRoom([]);
mockLogger = {
debug: jest.fn(),
warn: jest.fn(),
getChild: jest.fn(),
} as unknown as Mocked<Logger>;
mockLogger.getChild.mockReturnValue(mockLogger);
statistics = {
counters: {
roomEventEncryptionKeysSent: 0,
roomEventEncryptionKeysReceived: 0,
},
totals: {
roomEventEncryptionKeysReceivedTotalAge: 0,
},
};
roomKeyTransport = new RoomKeyTransport(mockRoom, mockClient, statistics);
toDeviceKeyTransport = new ToDeviceKeyTransport(
"@alice:example.org",
"MYDEVICE",
mockRoom.roomId,
mockClient,
statistics,
);
transport = new RoomAndToDeviceTransport(toDeviceKeyTransport, roomKeyTransport, mockLogger);
toDeviceSendKeySpy = jest.spyOn(toDeviceKeyTransport, "sendKey");
roomSendKeySpy = jest.spyOn(roomKeyTransport, "sendKey");
});
it("should enable to device transport when starting", () => {
transport.start();
expect(transport.enabled.room).toBeFalsy();
expect(transport.enabled.toDevice).toBeTruthy();
});
it("only sends to device keys when sending a key", async () => {
transport.start();
await transport.sendKey("1235", 0, [
{ userId: "@alice:example.org", deviceId: "ALICEDEVICE", membershipTs: 1234 },
]);
expect(toDeviceSendKeySpy).toHaveBeenCalledTimes(1);
expect(roomSendKeySpy).toHaveBeenCalledTimes(0);
expect(transport.enabled.room).toBeFalsy();
expect(transport.enabled.toDevice).toBeTruthy();
});
it("enables room transport and disables to device transport when receiving a room key", async () => {
transport.start();
const onNewKeyFromTransport = jest.fn();
const onTransportEnabled = jest.fn();
transport.on(KeyTransportEvents.ReceivedKeys, onNewKeyFromTransport);
transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, onTransportEnabled);
mockRoom.emit(
RoomEvent.Timeline,
makeMockEvent(EventType.CallEncryptionKeysPrefix, "@bob:example.org", roomId, {
call_id: "",
keys: [makeKey(0, "testKey")],
sent_ts: Date.now(),
device_id: "AAAAAAA",
}),
undefined,
undefined,
false,
{} as IRoomTimelineData,
);
await jest.advanceTimersByTimeAsync(1);
expect(transport.enabled.room).toBeTruthy();
expect(transport.enabled.toDevice).toBeFalsy();
await transport.sendKey("1235", 0, [
{ userId: "@alice:example.org", deviceId: "AlICEDEV", membershipTs: 1234 },
]);
expect(sendEventMock).toHaveBeenCalledTimes(1);
expect(roomSendKeySpy).toHaveBeenCalledTimes(1);
expect(toDeviceSendKeySpy).toHaveBeenCalledTimes(0);
expect(onTransportEnabled).toHaveBeenCalledWith({ toDevice: false, room: true });
});
it("enables room transport and disables to device transport on widget driver error", async () => {
mockClient.encryptAndSendToDevice.mockRejectedValue({
message:
"unknown variant `send_to_device`, expected one of `supported_api_versions`, `content_loaded`, `get_openid`, `org.matrix.msc2876.read_events`, `send_event`, `org.matrix.msc4157.update_delayed_event` at line 1 column 22",
});
transport.start();
const membership: ParticipantDeviceInfo = {
userId: "@alice:example.org",
deviceId: "ALICEDEVICE",
membershipTs: 1234,
};
const onTransportEnabled = jest.fn();
transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, onTransportEnabled);
// We start with toDevice transport enabled
expect(transport.enabled.room).toBeFalsy();
expect(transport.enabled.toDevice).toBeTruthy();
await transport.sendKey("1235", 0, [membership]);
// We switched transport, now room transport is enabled
expect(onTransportEnabled).toHaveBeenCalledWith({ toDevice: false, room: true });
expect(transport.enabled.room).toBeTruthy();
expect(transport.enabled.toDevice).toBeFalsy();
// sanity check that we called the failang to device send key.
expect(toDeviceKeyTransport.sendKey).toHaveBeenCalledWith("1235", 0, [membership]);
expect(toDeviceKeyTransport.sendKey).toHaveBeenCalledTimes(1);
// We re-sent the key via the room transport
expect(roomKeyTransport.sendKey).toHaveBeenCalledWith("1235", 0, [membership]);
expect(roomKeyTransport.sendKey).toHaveBeenCalledTimes(1);
mockClient.encryptAndSendToDevice.mockRestore();
});
it("does log that it did nothing when disabled", () => {
transport.start();
const onNewKeyFromTransport = jest.fn();
const onTransportEnabled = jest.fn();
transport.on(KeyTransportEvents.ReceivedKeys, onNewKeyFromTransport);
transport.on(RoomAndToDeviceEvents.EnabledTransportsChanged, onTransportEnabled);
transport.setEnabled({ toDevice: false, room: false });
const dateNow = Date.now();
roomKeyTransport.emit(KeyTransportEvents.ReceivedKeys, "user", "device", "roomKey", 0, dateNow);
toDeviceKeyTransport.emit(KeyTransportEvents.ReceivedKeys, "user", "device", "toDeviceKey", 0, Date.now());
expect(mockLogger.debug).toHaveBeenCalledWith("To Device transport is disabled, ignoring received keys");
// for room key transport we will never get a disabled message because its will always just turn on
expect(onTransportEnabled).toHaveBeenNthCalledWith(1, { toDevice: false, room: false });
expect(onTransportEnabled).toHaveBeenNthCalledWith(2, { toDevice: false, room: true });
expect(onNewKeyFromTransport).toHaveBeenCalledTimes(1);
expect(onNewKeyFromTransport).toHaveBeenCalledWith("user", "device", "roomKey", 0, dateNow);
});
});
+19 -18
View File
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type Mocked } from "vitest";
import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey } from "./mocks";
import { RoomKeyTransport } from "../../../src/matrixrtc/RoomKeyTransport";
import { KeyTransportEvents } from "../../../src/matrixrtc/IKeyTransport";
import { EventType, MatrixClient, RoomEvent } from "../../../src";
import { type IRoomTimelineData, MatrixEvent, type Room } from "../../../src";
import type { Mocked } from "jest-mock";
import type { Logger } from "../../../src/logger.ts";
describe("RoomKeyTransport", () => {
@@ -30,13 +31,13 @@ describe("RoomKeyTransport", () => {
let transport: RoomKeyTransport;
let mockLogger: Mocked<Logger>;
const onCallEncryptionMock = jest.fn();
const onCallEncryptionMock = vi.fn();
beforeEach(() => {
onCallEncryptionMock.mockReset();
mockLogger = {
debug: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
} as unknown as Mocked<Logger>;
const statistics = {
@@ -52,7 +53,7 @@ describe("RoomKeyTransport", () => {
client = new MatrixClient({ baseUrl: "base_url" });
client.matrixRTC.start();
transport = new RoomKeyTransport(room, client, statistics, {
getChild: jest.fn().mockReturnValue(mockLogger),
getChild: vi.fn().mockReturnValue(mockLogger),
} as unknown as Mocked<Logger>);
transport.on(KeyTransportEvents.ReceivedKeys, (...p) => {
onCallEncryptionMock(...p);
@@ -81,9 +82,9 @@ describe("RoomKeyTransport", () => {
describe("event decryption", () => {
it("Retries decryption and processes success", async () => {
jest.useFakeTimers();
vi.useFakeTimers();
let isDecryptionFailure = true;
client.decryptEventIfNeeded = jest
client.decryptEventIfNeeded = vi
.fn()
.mockReturnValueOnce(Promise.resolve())
.mockImplementation(() => {
@@ -98,7 +99,7 @@ describe("RoomKeyTransport", () => {
sent_ts: Date.now(),
device_id: "AAAAAAA",
}),
{ isDecryptionFailure: jest.fn().mockImplementation(() => isDecryptionFailure) },
{ isDecryptionFailure: vi.fn().mockImplementation(() => isDecryptionFailure) },
);
room.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
@@ -106,18 +107,18 @@ describe("RoomKeyTransport", () => {
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
// should retry after one second:
await jest.advanceTimersByTimeAsync(1500);
await vi.advanceTimersByTimeAsync(1500);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(1);
jest.useRealTimers();
vi.useRealTimers();
});
it("Retries decryption and processes failure", async () => {
try {
jest.useFakeTimers();
const onCallEncryptionMock = jest.fn();
client.decryptEventIfNeeded = jest.fn().mockReturnValue(Promise.resolve());
vi.useFakeTimers();
const onCallEncryptionMock = vi.fn();
client.decryptEventIfNeeded = vi.fn().mockReturnValue(Promise.resolve());
const timelineEvent = Object.assign(
makeMockEvent(EventType.CallEncryptionKeysPrefix, "@mock:user.example", "!room:id", {
@@ -126,7 +127,7 @@ describe("RoomKeyTransport", () => {
sent_ts: Date.now(),
device_id: "AAAAAAA",
}),
{ isDecryptionFailure: jest.fn().mockReturnValue(true) },
{ isDecryptionFailure: vi.fn().mockReturnValue(true) },
);
room.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
@@ -135,18 +136,18 @@ describe("RoomKeyTransport", () => {
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
// should retry after one second:
await jest.advanceTimersByTimeAsync(1500);
await vi.advanceTimersByTimeAsync(1500);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
// doesn't retry again:
await jest.advanceTimersByTimeAsync(1500);
await vi.advanceTimersByTimeAsync(1500);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
} finally {
jest.useRealTimers();
vi.useRealTimers();
}
});
});
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type Mocked } from "jest-mock";
import { type Mocked } from "vitest";
import { makeMockEvent } from "./mocks";
import { ClientEvent, EventType, type MatrixClient } from "../../../src";
@@ -34,11 +34,11 @@ describe("ToDeviceKeyTransport", () => {
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
encryptAndSendToDevice: jest.fn().mockImplementation(() => Promise.resolve()),
encryptAndSendToDevice: vi.fn().mockImplementation(() => Promise.resolve()),
});
mockLogger = {
debug: jest.fn(),
warn: jest.fn(),
debug: vi.fn(),
warn: vi.fn(),
} as unknown as Mocked<Logger>;
statistics = {
counters: {
@@ -50,9 +50,15 @@ describe("ToDeviceKeyTransport", () => {
},
};
transport = new ToDeviceKeyTransport("@alice:example.org", "MYDEVICE", roomId, mockClient, statistics, {
getChild: jest.fn().mockReturnValue(mockLogger),
} as unknown as Mocked<Logger>);
transport = new ToDeviceKeyTransport(
{ userId: "@alice:example.org", deviceId: "MYDEVICE", memberId: "@alice:example.org:MYDEVICE" },
roomId,
mockClient,
statistics,
{
getChild: vi.fn().mockReturnValue(mockLogger),
} as unknown as Mocked<Logger>,
);
});
it("should send my keys on via to device", async () => {
@@ -81,6 +87,7 @@ describe("ToDeviceKeyTransport", () => {
},
member: {
claimed_device_id: "MYDEVICE",
id: "@alice:example.org:MYDEVICE",
},
room_id: roomId,
session: {
@@ -102,8 +109,13 @@ describe("ToDeviceKeyTransport", () => {
keyBase64Encoded: string;
index: number;
}>();
transport.on(KeyTransportEvents.ReceivedKeys, (userId, deviceId, keyBase64Encoded, index, timestamp) => {
receivedKeyResolvers.resolve({ userId, deviceId, keyBase64Encoded, index });
transport.on(KeyTransportEvents.ReceivedKeys, (membership, keyBase64Encoded, index, _timestamp) => {
receivedKeyResolvers.resolve({
userId: membership.userId,
deviceId: membership.deviceId,
keyBase64Encoded,
index,
});
});
transport.start();
+92 -40
View File
@@ -15,13 +15,22 @@ limitations under the License.
*/
import { EventEmitter } from "stream";
import { type Mocked, type MockedObject } from "vitest";
import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src";
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc";
import { secureRandomString } from "../../../src/randomstring";
import { type CallMembershipIdentityParts } from "src/matrixrtc/EncryptionManager";
import { logger } from "../../../src/logger.ts";
export type MembershipData = (SessionMembershipData | {}) & { user_id: string };
export const owmMemberIdentity: CallMembershipIdentityParts = {
deviceId: "AAAAAAA",
memberId: "@alice:example.org:AAAAAAA",
userId: "@alice:example.org",
};
export const membershipTemplate: SessionMembershipData & { user_id: string } = {
application: "m.call",
call_id: "",
@@ -43,49 +52,68 @@ export const membershipTemplate: SessionMembershipData & { user_id: string } = {
],
};
export type MockClient = Pick<
MatrixClient,
| "getUserId"
| "getDeviceId"
| "sendEvent"
| "sendStateEvent"
| "_unstable_sendDelayedStateEvent"
| "_unstable_updateDelayedEvent"
| "cancelPendingEvent"
export type MockClient = MockedObject<
Pick<
MatrixClient,
| "getUserId"
| "getDeviceId"
| "sendEvent"
| "sendStateEvent"
| "_unstable_sendDelayedStateEvent"
| "_unstable_updateDelayedEvent"
| "_unstable_cancelScheduledDelayedEvent"
| "_unstable_restartScheduledDelayedEvent"
| "_unstable_sendScheduledDelayedEvent"
| "_unstable_sendStickyEvent"
| "_unstable_sendStickyDelayedEvent"
| "cancelPendingEvent"
>
>;
/**
* Mocks a object that has all required methods for a MatrixRTC session client.
*/
export function makeMockClient(userId: string, deviceId: string): MockClient {
return {
getDeviceId: () => deviceId,
getUserId: () => userId,
sendEvent: jest.fn(),
sendStateEvent: jest.fn(),
cancelPendingEvent: jest.fn(),
_unstable_updateDelayedEvent: jest.fn(),
_unstable_sendDelayedStateEvent: jest.fn(),
};
getDeviceId: vi.fn(() => deviceId),
getUserId: vi.fn(() => userId),
sendEvent: vi.fn(),
sendStateEvent: vi.fn(),
cancelPendingEvent: vi.fn(),
_unstable_updateDelayedEvent: vi.fn(),
_unstable_cancelScheduledDelayedEvent: vi.fn(),
_unstable_restartScheduledDelayedEvent: vi.fn(),
_unstable_sendScheduledDelayedEvent: vi.fn(),
_unstable_sendDelayedStateEvent: vi.fn(),
_unstable_sendStickyEvent: vi.fn(),
_unstable_sendStickyDelayedEvent: vi.fn(),
} as MockClient;
}
export function makeMockRoom(
membershipData: MembershipData[],
): Room & { emitTimelineEvent: (event: MatrixEvent) => void } {
useStickyEvents = false,
): Mocked<Room & { emitTimelineEvent: (event: MatrixEvent) => void }> {
const roomId = secureRandomString(8);
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
const roomState = makeMockRoomState(membershipData, roomId);
const roomState = makeMockRoomState(useStickyEvents ? [] : membershipData, roomId);
const ts = Date.now();
const room = Object.assign(new EventEmitter(), {
roomId: roomId,
hasMembershipState: jest.fn().mockReturnValue(true),
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue(roomState),
hasMembershipState: vi.fn().mockReturnValue(true),
getLiveTimeline: vi.fn().mockReturnValue({
getState: vi.fn().mockReturnValue(roomState),
}),
getVersion: jest.fn().mockReturnValue("default"),
}) as unknown as Room;
getVersion: vi.fn().mockReturnValue("default"),
_unstable_getStickyEvents: vi
.fn()
.mockImplementation(() =>
useStickyEvents ? membershipData.map((m) => mockRTCEvent(m, roomId, 10000, ts)) : [],
) as any,
});
return Object.assign(room, {
emitTimelineEvent: (event: MatrixEvent) =>
room.emit(RoomEvent.Timeline, event, room, undefined, false, {} as any),
});
}) as unknown as Mocked<Room & { emitTimelineEvent: (event: MatrixEvent) => void }>;
}
function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
@@ -96,8 +124,8 @@ function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
});
return {
on: jest.fn(),
off: jest.fn(),
on: vi.fn(),
off: vi.fn(),
getStateEvents: (_: string, stateKey: string) => {
if (stateKey !== undefined) return keysAndEvents.find(([k]) => k === stateKey)?.[1];
return events;
@@ -120,7 +148,7 @@ function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
}
export function mockRoomState(room: Room, membershipData: MembershipData[]): void {
room.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState(membershipData, room.roomId));
room.getLiveTimeline().getState = vi.fn().mockReturnValue(makeMockRoomState(membershipData, room.roomId));
}
export function makeMockEvent(
@@ -129,24 +157,48 @@ export function makeMockEvent(
roomId: string | undefined,
content: any,
timestamp?: number,
stateKey?: string,
): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(type),
getContent: jest.fn().mockReturnValue(content),
getSender: jest.fn().mockReturnValue(sender),
getTs: jest.fn().mockReturnValue(timestamp ?? Date.now()),
getRoomId: jest.fn().mockReturnValue(roomId),
getId: jest.fn().mockReturnValue(secureRandomString(8)),
isDecryptionFailure: jest.fn().mockReturnValue(false),
getType: vi.fn().mockReturnValue(type),
getContent: vi.fn().mockReturnValue(content),
getSender: vi.fn().mockReturnValue(sender),
getTs: vi.fn().mockReturnValue(timestamp ?? Date.now()),
getRoomId: vi.fn().mockReturnValue(roomId),
getId: vi.fn().mockReturnValue(secureRandomString(8)),
getStateKey: vi.fn().mockReturnValue(stateKey),
isDecryptionFailure: vi.fn().mockReturnValue(false),
} as unknown as MatrixEvent;
}
export function mockRTCEvent({ user_id: sender, ...membershipData }: MembershipData, roomId: string): MatrixEvent {
return makeMockEvent(EventType.GroupCallMemberPrefix, sender, roomId, membershipData);
export function mockRTCEvent(
{ user_id: sender, ...membershipData }: MembershipData,
roomId: string,
stickyDuration?: number,
timestamp?: number,
): MatrixEvent {
return {
...makeMockEvent(
stickyDuration !== undefined ? EventType.RTCMembership : EventType.GroupCallMemberPrefix,
sender,
roomId,
membershipData,
timestamp,
!stickyDuration && "device_id" in membershipData ? `_${sender}_${membershipData.device_id}` : "",
),
unstableStickyExpiresAt: stickyDuration,
} as unknown as MatrixEvent;
}
export function mockCallMembership(membershipData: MembershipData, roomId: string): CallMembership {
return new CallMembership(mockRTCEvent(membershipData, roomId), membershipData);
export function mockCallMembership(
membershipData: MembershipData,
roomId: string,
rtcBackendIdentity?: string,
): CallMembership {
const ev = mockRTCEvent(membershipData, roomId);
vi.mocked(ev.getContent).mockReturnValue(membershipData);
const data = CallMembership.membershipDataFromMatrixEvent(ev);
return new CallMembership(ev, data, rtcBackendIdentity ?? "xx", logger);
}
export function makeKey(id: number, key: string): { key: string; index: number } {
+135
View File
@@ -0,0 +1,135 @@
/*
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 { type CallMembership } from "../../../src/matrixrtc";
import { isMyMembership, parseCallNotificationContent } from "../../../src/matrixrtc/types";
describe("types", () => {
describe("isMyMembership", () => {
it("returns false if userId is different", () => {
expect(
isMyMembership(
{ sender: "@alice:example.org", deviceId: "DEVICE" } as CallMembership,
"@bob:example.org",
"DEVICE",
),
).toBe(false);
});
it("returns true if userId and device is the same", () => {
expect(
isMyMembership(
{ sender: "@alice:example.org", deviceId: "DEVICE" } as CallMembership,
"@alice:example.org",
"DEVICE",
),
).toBe(true);
});
});
});
describe("IRTCNotificationContent", () => {
const validBase = Object.freeze({
"m.mentions": { user_ids: [], room: true },
"notification_type": "notification",
"sender_ts": 123,
"lifetime": 1000,
});
it("parses valid content", () => {
const res = parseCallNotificationContent({ ...validBase });
expect(res).toMatchObject(validBase);
});
it("caps lifetime to 90000ms", () => {
const res = parseCallNotificationContent({ ...validBase, lifetime: 130000 });
expect(res.lifetime).toBe(90000);
});
it("throws on malformed m.mentions", () => {
expect(() =>
parseCallNotificationContent({
...validBase,
"m.mentions": "not an object",
} as any),
).toThrow("malformed m.mentions");
});
it("throws on missing or invalid notification_type", () => {
expect(() =>
parseCallNotificationContent({
...validBase,
notification_type: undefined,
} as any),
).toThrow("Missing or invalid notification_type");
expect(() =>
parseCallNotificationContent({
...validBase,
notification_type: 123 as any,
} as any),
).toThrow("Missing or invalid notification_type");
});
it("throws on missing or invalid sender_ts", () => {
expect(() =>
parseCallNotificationContent({
...validBase,
sender_ts: undefined,
} as any),
).toThrow("Missing or invalid sender_ts");
expect(() =>
parseCallNotificationContent({
...validBase,
sender_ts: "123" as any,
} as any),
).toThrow("Missing or invalid sender_ts");
});
it("throws on missing or invalid lifetime", () => {
expect(() =>
parseCallNotificationContent({
...validBase,
lifetime: undefined,
} as any),
).toThrow("Missing or invalid lifetime");
expect(() =>
parseCallNotificationContent({
...validBase,
lifetime: "1000" as any,
} as any),
).toThrow("Missing or invalid lifetime");
});
it("accepts valid relation (m.reference)", () => {
// Note: parseCallNotificationContent currently checks `relation.rel_type` rather than `m.relates_to`.
const res = parseCallNotificationContent({
...validBase,
relation: { rel_type: "m.reference", event_id: "$ev" },
} as any);
expect(res).toBeTruthy();
});
it("throws on invalid relation rel_type", () => {
expect(() =>
parseCallNotificationContent({
...validBase,
relation: { rel_type: "m.annotation", event_id: "$ev" },
} as any),
).toThrow("Invalid relation");
});
});
+8 -8
View File
@@ -85,7 +85,7 @@ describe("MSC3089Branch", () => {
it("should be able to delete the file", async () => {
const eventIdOrder = [fileEventId, fileEventId2];
const stateFn = jest
const stateFn = vi
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
@@ -98,7 +98,7 @@ describe("MSC3089Branch", () => {
});
client.sendStateEvent = stateFn;
const redactFn = jest.fn().mockImplementation((roomId: string, eventId: string) => {
const redactFn = vi.fn().mockImplementation((roomId: string, eventId: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventId).toEqual(eventIdOrder[stateFn.mock.calls.length - 1]);
@@ -127,7 +127,7 @@ describe("MSC3089Branch", () => {
it("should be able to change its name", async () => {
const name = "My File.txt";
indexEvent.getContent = () => ({ active: true, retained: true });
const stateFn = jest
const stateFn = vi
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
@@ -183,7 +183,7 @@ describe("MSC3089Branch", () => {
it("should be able to change its locked status", async () => {
const locked = true;
indexEvent.getContent = () => ({ active: true, retained: true });
const stateFn = jest
const stateFn = vi
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
@@ -259,20 +259,20 @@ describe("MSC3089Branch", () => {
const canaryAddl = { canary: true };
indexEvent.getContent = () => ({ active: true, retained: true });
const stateKeyOrder = [fileEventId2, fileEventId];
const stateFn = jest
const stateFn = vi
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(branchRoomId);
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
expect(stateKey).toEqual(stateKeyOrder[stateFn.mock.calls.length - 1]);
if (stateKey === fileEventId) {
// eslint-disable-next-line jest/no-conditional-expect
// eslint-disable-next-line @vitest/no-conditional-expect
expect(content).toMatchObject({
retained: true, // canary for copying state
active: false,
});
} else if (stateKey === fileEventId2) {
// eslint-disable-next-line jest/no-conditional-expect
// eslint-disable-next-line @vitest/no-conditional-expect
expect(content).toMatchObject({
active: true,
version: 2,
@@ -286,7 +286,7 @@ describe("MSC3089Branch", () => {
});
client.sendStateEvent = stateFn;
const createFn = jest
const createFn = vi
.fn()
.mockImplementation((name: string, contents: ArrayBuffer, info: Partial<EncryptedFile>, addl: IContent) => {
expect(name).toEqual(canaryName);
+36 -21
View File
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MockedFunction } from "vitest";
import { type IContent, type MatrixClient } from "../../../src";
import { type Room } from "../../../src/models/room";
import { MatrixEvent } from "../../../src/models/event";
@@ -85,7 +87,7 @@ describe("MSC3089TreeSpace", () => {
it("should support setting the name of the space", async () => {
const newName = "NEW NAME";
const fn = jest
const fn = vi
.fn()
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
expect(stateRoomId).toEqual(roomId);
@@ -101,7 +103,7 @@ describe("MSC3089TreeSpace", () => {
it("should support inviting users to the space", async () => {
const target = targetUser;
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
const fn = vi.fn().mockImplementation((inviteRoomId: string, userId: string) => {
expect(inviteRoomId).toEqual(roomId);
expect(userId).toEqual(target);
return Promise.resolve();
@@ -113,7 +115,7 @@ describe("MSC3089TreeSpace", () => {
it("should retry invites to the space", async () => {
const target = targetUser;
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
const fn = vi.fn().mockImplementation((inviteRoomId: string, userId: string) => {
expect(inviteRoomId).toEqual(roomId);
expect(userId).toEqual(target);
if (fn.mock.calls.length === 1) return Promise.reject(new Error("Sample Failure"));
@@ -126,7 +128,7 @@ describe("MSC3089TreeSpace", () => {
it("should not retry invite permission errors", async () => {
const target = targetUser;
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
const fn = vi.fn().mockImplementation((inviteRoomId: string, userId: string) => {
expect(inviteRoomId).toEqual(roomId);
expect(userId).toEqual(target);
return Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN", error: "Sample Failure" }));
@@ -140,7 +142,7 @@ describe("MSC3089TreeSpace", () => {
it("should invite to subspaces", async () => {
const target = targetUser;
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
const fn = vi.fn().mockImplementation((inviteRoomId: string, userId: string) => {
expect(inviteRoomId).toEqual(roomId);
expect(userId).toEqual(target);
return Promise.resolve();
@@ -161,7 +163,7 @@ describe("MSC3089TreeSpace", () => {
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
makePowerLevels(pls);
const fn = jest
const fn = vi
.fn()
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
expect(stateRoomId).toEqual(roomId);
@@ -187,6 +189,7 @@ describe("MSC3089TreeSpace", () => {
expect(finalPermissions).toEqual(role);
}
// eslint-disable-next-line @vitest/expect-expect
it("should support setting Viewer permissions", () => {
return evaluatePowerLevels(
{
@@ -202,6 +205,7 @@ describe("MSC3089TreeSpace", () => {
);
});
// eslint-disable-next-line @vitest/expect-expect
it("should support setting Editor permissions", () => {
return evaluatePowerLevels(
{
@@ -217,6 +221,7 @@ describe("MSC3089TreeSpace", () => {
);
});
// eslint-disable-next-line @vitest/expect-expect
it("should support setting Owner permissions", () => {
return evaluatePowerLevels(
{
@@ -232,6 +237,7 @@ describe("MSC3089TreeSpace", () => {
);
});
// eslint-disable-next-line @vitest/expect-expect
it("should support demoting permissions", () => {
return evaluatePowerLevels(
{
@@ -250,6 +256,7 @@ describe("MSC3089TreeSpace", () => {
);
});
// eslint-disable-next-line @vitest/expect-expect
it("should support promoting permissions", () => {
return evaluatePowerLevels(
{
@@ -268,14 +275,17 @@ describe("MSC3089TreeSpace", () => {
);
});
// eslint-disable-next-line @vitest/expect-expect
it("should support defaults: Viewer", () => {
return evaluatePowerLevels({}, TreePermissions.Viewer, 0);
});
// eslint-disable-next-line @vitest/expect-expect
it("should support defaults: Editor", () => {
return evaluatePowerLevels({}, TreePermissions.Editor, 50);
});
// eslint-disable-next-line @vitest/expect-expect
it("should support defaults: Owner", () => {
return evaluatePowerLevels({}, TreePermissions.Owner, 100);
});
@@ -294,11 +304,11 @@ describe("MSC3089TreeSpace", () => {
}
};
client.getDomain = () => domain;
const createFn = jest.fn().mockImplementation(async (name: string) => {
const createFn = vi.fn().mockImplementation(async (name: string) => {
expect(name).toEqual(subspaceName);
return new MSC3089TreeSpace(client, subspaceId);
});
const sendStateFn = jest
const sendStateFn = vi
.fn()
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
expect([tree.roomId, subspaceId]).toContain(roomId);
@@ -352,7 +362,7 @@ describe("MSC3089TreeSpace", () => {
};
client.getRoom = () => ({}) as Room; // to appease the TreeSpace constructor
const getFn = jest.fn().mockImplementation((roomId: string) => {
const getFn = vi.fn().mockImplementation((roomId: string) => {
if (roomId === thirdChildRoom) {
throw new Error("Mock not-a-space room case called (expected)");
}
@@ -389,10 +399,10 @@ describe("MSC3089TreeSpace", () => {
});
it("should be able to delete itself", async () => {
const delete1 = jest.fn().mockImplementation(() => Promise.resolve());
const delete1 = vi.fn().mockImplementation(() => Promise.resolve());
const subdir1 = { delete: delete1 } as any as MSC3089TreeSpace; // mock tested bits
const delete2 = jest.fn().mockImplementation(() => Promise.resolve());
const delete2 = vi.fn().mockImplementation(() => Promise.resolve());
const subdir2 = { delete: delete2 } as any as MSC3089TreeSpace; // mock tested bits
const joinMemberId = "@join:example.org";
@@ -422,8 +432,8 @@ describe("MSC3089TreeSpace", () => {
};
// These two functions are tested by input expectations, so no expectations in the function bodies
const kickFn = jest.fn().mockImplementation((userId) => Promise.resolve());
const leaveFn = jest.fn().mockImplementation(() => Promise.resolve());
const kickFn = vi.fn().mockImplementation((userId) => Promise.resolve());
const leaveFn = vi.fn().mockImplementation(() => Promise.resolve());
client.kick = kickFn;
client.leave = leaveFn;
client.getUserId = () => selfUserId;
@@ -449,7 +459,7 @@ describe("MSC3089TreeSpace", () => {
let parentRoom: Room;
let childTrees: MSC3089TreeSpace[];
let rooms: { [roomId: string]: Room };
let clientSendStateFn: jest.MockedFunction<typeof client.sendStateEvent>;
let clientSendStateFn: MockedFunction<typeof client.sendStateEvent>;
const staticDomain = "static.example.org";
function addSubspace(roomId: string, createTs?: number, order?: string) {
@@ -540,7 +550,7 @@ describe("MSC3089TreeSpace", () => {
(<any>tree).room = parentRoom; // override readonly
client.getRoom = (r) => rooms[r ?? ""];
clientSendStateFn = jest
clientSendStateFn = vi
.fn()
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
expect(roomId).toEqual(tree.roomId);
@@ -583,6 +593,7 @@ describe("MSC3089TreeSpace", () => {
await expect(tree.setOrder(2)).rejects.toThrow("Cannot set order of top level spaces currently");
});
// eslint-disable-next-line @vitest/expect-expect
it("should return a stable order for unordered children", () => {
const a = "!a:example.org";
const b = "!b:example.org";
@@ -598,6 +609,7 @@ describe("MSC3089TreeSpace", () => {
expectOrder(c, 2);
});
// eslint-disable-next-line @vitest/expect-expect
it("should return a stable order for ordered children", () => {
const a = "!a:example.org";
const b = "!b:example.org";
@@ -613,6 +625,7 @@ describe("MSC3089TreeSpace", () => {
expectOrder(a, 2);
});
// eslint-disable-next-line @vitest/expect-expect
it("should return a stable order for partially ordered children", () => {
const a = "!a:example.org";
const b = "!b:example.org";
@@ -631,6 +644,7 @@ describe("MSC3089TreeSpace", () => {
expectOrder(a, 2);
});
// eslint-disable-next-line @vitest/expect-expect
it("should return a stable order if the create event timestamps are the same", () => {
const a = "!a:example.org";
const b = "!b:example.org";
@@ -646,6 +660,7 @@ describe("MSC3089TreeSpace", () => {
expectOrder(c, 2);
});
// eslint-disable-next-line @vitest/expect-expect
it("should return a stable order if there are no known create events", () => {
const a = "!a:example.org";
const b = "!b:example.org";
@@ -902,7 +917,7 @@ describe("MSC3089TreeSpace", () => {
const fileName = "My File.txt";
const fileContents = "This is a test file";
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
const uploadFn = vi.fn().mockImplementation((contents: Buffer, opts: any) => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
@@ -911,7 +926,7 @@ describe("MSC3089TreeSpace", () => {
});
client.uploadContent = uploadFn;
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
const sendMsgFn = vi.fn().mockImplementation((roomId: string, contents: any) => {
expect(roomId).toEqual(tree.roomId);
expect(contents).toMatchObject({
msgtype: MsgType.File,
@@ -926,7 +941,7 @@ describe("MSC3089TreeSpace", () => {
});
client.sendMessage = sendMsgFn;
const sendStateFn = jest
const sendStateFn = vi
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(tree.roomId);
@@ -962,7 +977,7 @@ describe("MSC3089TreeSpace", () => {
const fileName = "My File.txt";
const fileContents = "This is a test file";
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
const uploadFn = vi.fn().mockImplementation((contents: Buffer, opts: any) => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
@@ -971,7 +986,7 @@ describe("MSC3089TreeSpace", () => {
});
client.uploadContent = uploadFn;
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
const sendMsgFn = vi.fn().mockImplementation((roomId: string, contents: any) => {
expect(roomId).toEqual(tree.roomId);
const content = {
msgtype: MsgType.File,
@@ -989,7 +1004,7 @@ describe("MSC3089TreeSpace", () => {
});
client.sendMessage = sendMsgFn;
const sendStateFn = jest
const sendStateFn = vi
.fn()
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
expect(roomId).toEqual(tree.roomId);
+19 -19
View File
@@ -20,7 +20,7 @@ import { M_BEACON_INFO } from "../../../src/@types/beacon";
import { isTimestampInDuration, Beacon, BeaconEvent } from "../../../src/models/beacon";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils/beacon";
jest.useFakeTimers();
vi.useFakeTimers();
describe("Beacon", () => {
describe("isTimestampInDuration()", () => {
@@ -70,9 +70,9 @@ describe("Beacon", () => {
const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
jest.spyOn(globalThis.Date, "now").mockReturnValue(Date.now() + ms);
vi.spyOn(globalThis.Date, "now").mockReturnValue(Date.now() + ms);
// then advance time for the interval by the same amount
jest.advanceTimersByTime(ms);
vi.advanceTimersByTime(ms);
};
beforeEach(() => {
@@ -108,11 +108,11 @@ describe("Beacon", () => {
);
// back to 'now'
jest.spyOn(globalThis.Date, "now").mockReturnValue(now);
vi.spyOn(globalThis.Date, "now").mockReturnValue(now);
});
afterAll(() => {
jest.spyOn(globalThis.Date, "now").mockRestore();
vi.spyOn(globalThis.Date, "now").mockRestore();
});
it("creates beacon from event", () => {
@@ -250,7 +250,7 @@ describe("Beacon", () => {
it("does not update with an older event", () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, "emit").mockClear();
const emitSpy = vi.spyOn(beacon, "emit").mockClear();
expect(beacon.beaconInfoId).toEqual(liveBeaconEvent.getId());
const oldUpdateEvent = makeBeaconInfoEvent(userId, roomId);
@@ -265,7 +265,7 @@ describe("Beacon", () => {
it("updates event", () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
expect(beacon.isLive).toEqual(true);
@@ -283,7 +283,7 @@ describe("Beacon", () => {
it("emits livenesschange event when beacon liveness changes", () => {
const beacon = new Beacon(liveBeaconEvent);
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
expect(beacon.isLive).toEqual(true);
@@ -305,7 +305,7 @@ describe("Beacon", () => {
// beacon was created an hour ago
// and has a 3hr duration
const beacon = new Beacon(notLiveBeaconEvent);
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
beacon.monitorLiveness();
@@ -332,7 +332,7 @@ describe("Beacon", () => {
const beacon = new Beacon(futureBeaconEvent);
expect(beacon.isLive).toBeFalsy();
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
beacon.monitorLiveness();
@@ -356,7 +356,7 @@ describe("Beacon", () => {
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
beacon.monitorLiveness();
advanceDateAndTime(HOUR_MS * 2 + 1);
@@ -386,7 +386,7 @@ describe("Beacon", () => {
// and has a 3hr duration
const beacon = new Beacon(liveBeaconEvent);
expect(beacon.isLive).toBeTruthy();
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
beacon.monitorLiveness();
@@ -406,7 +406,7 @@ describe("Beacon", () => {
describe("addLocations", () => {
it("ignores locations when beacon is not live", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: false }));
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
beacon.addLocations([
makeBeaconEvent(userId, { beaconInfoId: beacon.beaconInfoId, timestamp: now + 1 }),
@@ -418,7 +418,7 @@ describe("Beacon", () => {
it("ignores locations outside the beacon live duration", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
beacon.addLocations([
// beacon has now + 60000 live period
@@ -431,7 +431,7 @@ describe("Beacon", () => {
it("should ignore invalid beacon events", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
const ev = new MatrixEvent({
type: M_BEACON_INFO.name,
@@ -460,7 +460,7 @@ describe("Beacon", () => {
timestamp: startTimestamp,
}),
);
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
beacon.addLocations([
// beacon has now + 60000 live period
@@ -483,7 +483,7 @@ describe("Beacon", () => {
timestamp: startTimestamp,
}),
);
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
beacon.addLocations([
// beacon has now + 600000 live period
@@ -501,7 +501,7 @@ describe("Beacon", () => {
it("sets latest location state to most recent location", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, "emit");
const emitSpy = vi.spyOn(beacon, "emit");
const locations = [
// older
@@ -553,7 +553,7 @@ describe("Beacon", () => {
);
expect(beacon.latestLocationEvent).toEqual(newerLocation);
const emitSpy = jest.spyOn(beacon, "emit").mockClear();
const emitSpy = vi.spyOn(beacon, "emit").mockClear();
// add older location
beacon.addLocations([olderLocation]);
+34 -21
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type MockedObject } from "jest-mock";
import { type MockedObject } from "vitest";
import { type IContent, MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils";
@@ -159,7 +159,7 @@ describe("MatrixEvent", () => {
// Then it disappears from the thread and appears in the main timeline
expect(ev.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId()]);
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), redaction.getId()]);
expect(threadLiveEventIds(room, 0)).not.toContain(ev.getId());
});
@@ -183,7 +183,12 @@ describe("MatrixEvent", () => {
// Then the reaction moves into the main timeline
expect(reaction.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), reaction.getId()]);
expect(mainTimelineLiveEventIds(room)).toEqual([
threadRoot.getId(),
ev.getId(),
reaction.getId(),
redaction.getId(),
]);
expect(threadLiveEventIds(room, 0)).not.toContain(reaction.getId());
});
@@ -207,7 +212,12 @@ describe("MatrixEvent", () => {
// Then the edit moves into the main timeline
expect(edit.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), edit.getId()]);
expect(mainTimelineLiveEventIds(room)).toEqual([
threadRoot.getId(),
ev.getId(),
edit.getId(),
redaction.getId(),
]);
expect(threadLiveEventIds(room, 0)).not.toContain(edit.getId());
});
@@ -245,6 +255,7 @@ describe("MatrixEvent", () => {
reply1.getId(),
reply2.getId(),
reaction.getId(),
redaction.getId(),
]);
expect(threadLiveEventIds(room, 0)).not.toContain(reply1.getId());
expect(threadLiveEventIds(room, 0)).not.toContain(reply2.getId());
@@ -253,9 +264,9 @@ describe("MatrixEvent", () => {
function createMockClient(): MatrixClient {
return {
supportsThreads: jest.fn().mockReturnValue(true),
decryptEventIfNeeded: jest.fn().mockReturnThis(),
getUserId: jest.fn().mockReturnValue("@user:server"),
supportsThreads: vi.fn().mockReturnValue(true),
decryptEventIfNeeded: vi.fn().mockReturnThis(),
getUserId: vi.fn().mockReturnValue("@user:server"),
} as unknown as MockedObject<MatrixClient>;
}
@@ -330,6 +341,7 @@ describe("MatrixEvent", () => {
function createRedaction(redactedEventid: string): MatrixEvent {
return new MatrixEvent({
event_id: `$redact-${redactedEventid}`,
type: "m.room.redaction",
redacts: redactedEventid,
});
@@ -362,6 +374,7 @@ describe("MatrixEvent", () => {
});
describe("applyVisibilityEvent", () => {
// eslint-disable-next-line @vitest/expect-expect
it("should emit VisibilityChange if a change was made", async () => {
const ev = new MatrixEvent({
type: "m.room.message",
@@ -392,12 +405,12 @@ describe("MatrixEvent", () => {
});
it("should report unknown decryption errors", async () => {
const decryptionListener = jest.fn();
const decryptionListener = vi.fn();
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
const testError = new Error("test error");
const crypto = {
decryptEvent: jest.fn().mockRejectedValue(testError),
decryptEvent: vi.fn().mockRejectedValue(testError),
} as unknown as CryptoBackend;
await encryptedEvent.attemptDecryption(crypto);
@@ -408,7 +421,7 @@ describe("MatrixEvent", () => {
expect(encryptedEvent.decryptionFailureReason).not.toBe(
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
);
expect(encryptedEvent.getContent()).toEqual({
expect(encryptedEvent.getContent<IContent>()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: Error: test error **",
});
@@ -416,12 +429,12 @@ describe("MatrixEvent", () => {
});
it("should report known decryption errors", async () => {
const decryptionListener = jest.fn();
const decryptionListener = vi.fn();
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
const testError = new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, "uisi");
const crypto = {
decryptEvent: jest.fn().mockRejectedValue(testError),
decryptEvent: vi.fn().mockRejectedValue(testError),
} as unknown as CryptoBackend;
await encryptedEvent.attemptDecryption(crypto);
@@ -434,7 +447,7 @@ describe("MatrixEvent", () => {
expect(encryptedEvent.decryptionFailureReason).not.toBe(
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
);
expect(encryptedEvent.getContent()).toEqual({
expect(encryptedEvent.getContent<IContent>()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: DecryptionError: uisi **",
});
@@ -443,7 +456,7 @@ describe("MatrixEvent", () => {
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
const crypto = {
decryptEvent: jest
decryptEvent: vi
.fn()
.mockRejectedValue(
new DecryptionError(
@@ -460,17 +473,17 @@ describe("MatrixEvent", () => {
expect(encryptedEvent.decryptionFailureReason).toBe(
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
);
expect(encryptedEvent.getContent()).toEqual({
expect(encryptedEvent.getContent<IContent>()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: DecryptionError: The sender has disabled encrypting to unverified devices. **",
});
});
it("should retry decryption if a retry is queued", async () => {
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
const eventAttemptDecryptionSpy = vi.spyOn(encryptedEvent, "attemptDecryption");
const crypto = {
decryptEvent: jest
decryptEvent: vi
.fn()
.mockImplementationOnce(() => {
// schedule a second decryption attempt while
@@ -512,7 +525,7 @@ describe("MatrixEvent", () => {
});
const crypto = {
decryptEvent: jest.fn().mockImplementationOnce(() => {
decryptEvent: vi.fn().mockImplementationOnce(() => {
return Promise.resolve<EventDecryptionResult>({
clearEvent: {
type: "m.room.message",
@@ -615,8 +628,8 @@ describe("MatrixEvent", () => {
},
};
try {
jest.useFakeTimers();
jest.setSystemTime(50);
vi.useFakeTimers();
vi.setSystemTime(50);
// Prefer unsigned
expect(new MatrixEvent({ ...evData } satisfies IStickyEvent).unstableStickyExpiresAt).toEqual(5050);
// Fall back to `duration_ms`
@@ -629,7 +642,7 @@ describe("MatrixEvent", () => {
.unstableStickyExpiresAt,
).toEqual(1050);
} finally {
jest.useRealTimers();
vi.useRealTimers();
}
});
});
+28 -26
View File
@@ -25,18 +25,18 @@ import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test
import { flushPromises } from "../../test-utils/flushPromises";
import { mkEvent } from "../../test-utils/test-utils";
jest.useFakeTimers();
vi.useFakeTimers();
describe("Poll", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
decryptEventIfNeeded: jest.fn().mockResolvedValue(true),
relations: jest.fn(),
decryptEventIfNeeded: vi.fn().mockResolvedValue(true),
relations: vi.fn(),
});
const roomId = "!room:server";
const room = new Room(roomId, mockClient, userId);
const maySendRedactionForEventSpy = jest.spyOn(room.currentState, "maySendRedactionForEvent");
const maySendRedactionForEventSpy = vi.spyOn(room.currentState, "maySendRedactionForEvent");
// 14.03.2022 16:15
const now = 1647270879403;
@@ -48,8 +48,8 @@ describe("Poll", () => {
basePollStartEvent.event.event_id = "$12345";
beforeEach(() => {
jest.clearAllMocks();
jest.setSystemTime(now);
vi.clearAllMocks();
vi.setSystemTime(now);
mockClient.relations.mockReset().mockResolvedValue({ events: [] });
@@ -100,7 +100,7 @@ describe("Poll", () => {
describe("fetching responses", () => {
it("calls relations api and emits", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
const emitSpy = jest.spyOn(poll, "emit");
const emitSpy = vi.spyOn(poll, "emit");
const fetchResponsePromise = poll.getResponses();
expect(poll.isFetchingResponses).toBe(true);
const responses = await fetchResponsePromise;
@@ -127,15 +127,17 @@ describe("Poll", () => {
it("waits for existing relations request to finish when getting responses", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
// @ts-expect-error TS2769
const spy = vi.spyOn(poll, "fetchResponses");
const firstResponsePromise = poll.getResponses();
const secondResponsePromise = poll.getResponses();
await firstResponsePromise;
expect(firstResponsePromise).toEqual(secondResponsePromise);
await secondResponsePromise;
expect(spy).toHaveBeenCalledTimes(1);
expect(mockClient.relations).toHaveBeenCalledTimes(1);
});
it("filters relations for relevent response events", async () => {
it("filters relations for relevant response events", async () => {
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
@@ -175,7 +177,7 @@ describe("Poll", () => {
});
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
const responses = await poll.getResponses();
await flushPromises();
@@ -197,13 +199,13 @@ describe("Poll", () => {
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
vi.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
mockClient.relations.mockResolvedValue({
events: [replyEvent, stableResponseEvent, undecryptableEvent],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
await poll.getResponses();
expect(poll.undecryptableRelationsCount).toBe(1);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 1);
@@ -214,14 +216,14 @@ describe("Poll", () => {
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
const undecryptableEvent2 = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
jest.spyOn(undecryptableEvent2, "isDecryptionFailure").mockReturnValue(true);
vi.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
vi.spyOn(undecryptableEvent2, "isDecryptionFailure").mockReturnValue(true);
mockClient.relations.mockResolvedValue({
events: [replyEvent, stableResponseEvent, undecryptableEvent],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
await poll.getResponses();
expect(poll.undecryptableRelationsCount).toBe(1);
@@ -248,7 +250,7 @@ describe("Poll", () => {
it("sets poll end event with stable event type", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
await poll.getResponses();
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
@@ -270,7 +272,7 @@ describe("Poll", () => {
events: [pollEndEvent],
});
maySendRedactionForEventSpy.mockReturnValue(false);
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
await poll.getResponses();
expect(maySendRedactionForEventSpy).not.toHaveBeenCalled();
@@ -283,7 +285,7 @@ describe("Poll", () => {
events: [unstablePollEndEvent],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
await poll.getResponses();
expect(poll.isEnded).toBe(true);
@@ -304,7 +306,7 @@ describe("Poll", () => {
describe("onNewRelation()", () => {
it("discards response if poll responses have not been initialised", () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
poll.onNewRelation(responseEvent);
@@ -315,7 +317,7 @@ describe("Poll", () => {
it("sets poll end event when responses are not initialised", () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
poll.onNewRelation(stablePollEndEvent);
@@ -332,7 +334,7 @@ describe("Poll", () => {
});
const poll = new Poll(basePollStartEvent, mockClient, room);
await poll.getResponses();
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
poll.onNewRelation(stablePollEndEvent);
@@ -367,7 +369,7 @@ describe("Poll", () => {
expect(poll.isEnded).toBeTruthy();
// reset spy count
jest.spyOn(poll, "emit").mockClear();
vi.spyOn(poll, "emit").mockClear();
// add a valid end event with earlier timestamp
poll.onNewRelation(earlierPollEndEvent);
@@ -398,7 +400,7 @@ describe("Poll", () => {
expect(poll.isEnded).toBeTruthy();
// reset spy count
jest.spyOn(poll, "emit").mockClear();
vi.spyOn(poll, "emit").mockClear();
poll.onNewRelation(laterPollEndEvent);
// didn't set new end event, didn't refilter responses
@@ -416,7 +418,7 @@ describe("Poll", () => {
});
const poll = new Poll(basePollStartEvent, mockClient, room);
const responses = await poll.getResponses();
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
expect(responses.getRelations().length).toEqual(3);
poll.onNewRelation(stablePollEndEvent);
@@ -432,7 +434,7 @@ describe("Poll", () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
// init responses
const responses = await poll.getResponses();
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
const replyEvent = new MatrixEvent({ type: "m.room.message" });
poll.onNewRelation(replyEvent);
@@ -446,7 +448,7 @@ describe("Poll", () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
// init responses
const responses = await poll.getResponses();
jest.spyOn(poll, "emit");
vi.spyOn(poll, "emit");
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
poll.onNewRelation(responseEvent);
+9 -9
View File
@@ -32,11 +32,11 @@ import { Room } from "../../../src/models/room";
*/
describe("RoomReceipts", () => {
beforeAll(() => {
jest.replaceProperty(Thread, "hasServerSideSupport", FeatureSupport.Stable);
vi.spyOn(Thread, "hasServerSideSupport", "get").mockReturnValue(FeatureSupport.Stable);
});
afterAll(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it("reports events unread if there are no receipts", () => {
@@ -436,13 +436,13 @@ describe("RoomReceipts", () => {
function createFakeClient(): MatrixClient {
return {
getUserId: jest.fn(),
getEventMapper: jest.fn().mockReturnValue(jest.fn()),
isInitialSyncComplete: jest.fn().mockReturnValue(true),
supportsThreads: jest.fn().mockReturnValue(true),
fetchRoomEvent: jest.fn().mockResolvedValue({}),
paginateEventTimeline: jest.fn(),
canSupport: { get: jest.fn() },
getUserId: vi.fn(),
getEventMapper: vi.fn().mockReturnValue(vi.fn()),
isInitialSyncComplete: vi.fn().mockReturnValue(true),
supportsThreads: vi.fn().mockReturnValue(true),
fetchRoomEvent: vi.fn().mockResolvedValue({}),
paginateEventTimeline: vi.fn(),
canSupport: { get: vi.fn() },
} as unknown as MatrixClient;
}
+175 -13
View File
@@ -1,9 +1,11 @@
import { type Mock } from "vitest";
import { type IStickyEvent, MatrixEvent } from "../../../src";
import { RoomStickyEventsStore, RoomStickyEventsEvent } from "../../../src/models/room-sticky-events";
describe("RoomStickyEvents", () => {
let stickyEvents: RoomStickyEventsStore;
const emitSpy: jest.Mock = jest.fn();
const emitSpy: Mock = vi.fn();
const stickyEvent: IStickyEvent = {
event_id: "$foo:bar",
room_id: "!roomId",
@@ -196,14 +198,14 @@ describe("RoomStickyEvents", () => {
describe("cleanExpiredStickyEvents", () => {
beforeAll(() => {
jest.useFakeTimers();
vi.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
vi.useRealTimers();
});
it("should emit when a sticky event expires", () => {
jest.setSystemTime(1000);
vi.setSystemTime(1000);
const ev = new MatrixEvent({
...stickyEvent,
origin_server_ts: 0,
@@ -215,18 +217,18 @@ describe("RoomStickyEvents", () => {
origin_server_ts: 1000,
});
stickyEvents.addStickyEvents([ev, evLater]);
const emitSpy = jest.fn();
const emitSpy = vi.fn();
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
jest.advanceTimersByTime(15000);
vi.advanceTimersByTime(15000);
expect(emitSpy).toHaveBeenCalledWith([], [], [ev]);
// Then expire the next event
jest.advanceTimersByTime(1000);
vi.advanceTimersByTime(1000);
expect(emitSpy).toHaveBeenCalledWith([], [], [evLater]);
});
it("should emit two events when both expire at the same time", () => {
const emitSpy = jest.fn();
const emitSpy = vi.fn();
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
jest.setSystemTime(0);
vi.setSystemTime(0);
const ev1 = new MatrixEvent({
...stickyEvent,
event_id: "$eventA",
@@ -242,21 +244,181 @@ describe("RoomStickyEvents", () => {
});
stickyEvents.addStickyEvents([ev1, ev2]);
expect(emitSpy).toHaveBeenCalledWith([ev1, ev2], [], []);
jest.advanceTimersByTime(15000);
vi.advanceTimersByTime(15000);
expect(emitSpy).toHaveBeenCalledWith([], [], [ev1, ev2]);
});
it("should emit when a unkeyed sticky event expires", () => {
const emitSpy = jest.fn();
const emitSpy = vi.fn();
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
jest.setSystemTime(0);
vi.setSystemTime(0);
const ev = new MatrixEvent({
...stickyEvent,
content: {},
origin_server_ts: Date.now(),
});
stickyEvents.addStickyEvents([ev]);
jest.advanceTimersByTime(15000);
vi.advanceTimersByTime(15000);
expect(emitSpy).toHaveBeenCalledWith([], [], [ev]);
});
});
describe("handleRedaction", () => {
beforeAll(() => {
vi.useFakeTimers();
});
afterAll(() => {
vi.useRealTimers();
});
it("should not emit if the event does not exist in the map", () => {
const emitSpy = vi.fn();
const ev = new MatrixEvent({
...stickyEvent,
content: {},
origin_server_ts: Date.now(),
});
stickyEvents.addStickyEvents([ev]);
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
stickyEvents.handleRedaction("$123456");
expect(emitSpy).not.toHaveBeenCalled();
});
it("should emit a remove when the event exists in the map without a predecessor", () => {
const emitSpy = vi.fn();
const ev = new MatrixEvent({
...stickyEvent,
origin_server_ts: Date.now(),
});
stickyEvents.addStickyEvents([ev]);
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
stickyEvents.handleRedaction(stickyEvent.event_id);
expect(emitSpy).toHaveBeenCalledWith([], [], [ev]);
});
it("should emit a remove when the event has no sticky key", () => {
const emitSpy = vi.fn();
const ev = new MatrixEvent({
...stickyEvent,
content: {},
origin_server_ts: Date.now(),
});
stickyEvents.addStickyEvents([ev]);
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
stickyEvents.handleRedaction(stickyEvent.event_id);
expect(emitSpy).toHaveBeenCalledWith([], [], [ev]);
});
it("should emit an update when the event exists in the map with a predecessor", () => {
const emitSpy = vi.fn();
const ev = new MatrixEvent({
...stickyEvent,
origin_server_ts: Date.now(),
});
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
const newerEv = new MatrixEvent({
...stickyEvent,
event_id: "$newer-ev",
origin_server_ts: Date.now() + 1000,
});
stickyEvents.addStickyEvents([ev, newerEv]);
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
stickyEvents.handleRedaction(newerEv.getId()!);
expect(emitSpy).toHaveBeenCalledWith([], [{ current: ev, previous: newerEv }], []);
});
it("should emit a remove if the previous event has expired", () => {
const emitSpy = vi.fn();
const ev = new MatrixEvent({
...stickyEvent,
origin_server_ts: Date.now(),
});
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
const newerEv = new MatrixEvent({
...stickyEvent,
event_id: "$newer-ev",
origin_server_ts: Date.now() + 1000,
});
stickyEvents.addStickyEvents([ev, newerEv]);
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
// Expire the older event.
vi.advanceTimersByTime(stickyEvent.msc4354_sticky.duration_ms);
// Redact the newer event
stickyEvents.handleRedaction(newerEv.getId()!);
expect(emitSpy).toHaveBeenCalledWith([], [], [newerEv]);
});
it("should recurse the chain of events if the previous event has been redacted", () => {
const emitSpy = vi.fn();
const ev = new MatrixEvent({
...stickyEvent,
origin_server_ts: Date.now(),
});
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
const middleEv = new MatrixEvent({
...stickyEvent,
event_id: "$newer-ev",
origin_server_ts: Date.now() + 1000,
});
vi.advanceTimersByTime(1000);
const newestEv = new MatrixEvent({
...stickyEvent,
event_id: "$newest-ev",
origin_server_ts: Date.now() + 2000,
});
stickyEvents.addStickyEvents([ev, middleEv, newestEv]);
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
// Mark the middle event as redacted.
middleEv.setUnsigned({
redacted_because: {
event_id: "$foo",
} as any,
});
// Redact the newer event
stickyEvents.handleRedaction(newestEv.getId()!);
// expect immediate transition from newestEv -> ev and skipping middleEv
expect(emitSpy).toHaveBeenCalledWith([], [{ current: ev, previous: newestEv }], []);
});
it("should revert to the most recent valid event regardless of insertion order", () => {
const emitSpy = vi.fn();
const ev = new MatrixEvent({
...stickyEvent,
origin_server_ts: Date.now(),
});
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
const middleEv = new MatrixEvent({
...stickyEvent,
event_id: "$newer-ev",
origin_server_ts: Date.now() + 1000,
});
vi.advanceTimersByTime(1000);
const newestEv = new MatrixEvent({
...stickyEvent,
event_id: "$newest-ev",
origin_server_ts: Date.now() + 2000,
});
// Invert in reverse order, to make sure we retain the older events.
stickyEvents.addStickyEvents([newestEv, middleEv, ev]);
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
// Mark the middle event as redacted.
middleEv.setUnsigned({
redacted_because: {
event_id: "$foo",
} as any,
});
// Redact the newer event
stickyEvents.handleRedaction(newestEv.getId()!);
expect(emitSpy).toHaveBeenCalledWith([], [{ current: ev, previous: newestEv }], []);
});
it("should handle redaction when using `handleRedaction` with a `MatrixEvent` parameter", () => {
const emitSpy = vi.fn();
const ev = new MatrixEvent({
...stickyEvent,
origin_server_ts: Date.now(),
});
vi.advanceTimersByTime(1000); // Advance time so we can insert a newer event.
const newerEv = new MatrixEvent({
...stickyEvent,
event_id: "$newer-ev",
origin_server_ts: Date.now() + 1000,
});
stickyEvents.addStickyEvents([ev, newerEv]);
stickyEvents.on(RoomStickyEventsEvent.Update, emitSpy);
stickyEvents.handleRedaction(newerEv);
expect(emitSpy).toHaveBeenCalledWith([], [{ current: ev, previous: newerEv }], []);
});
});
});
+80 -5
View File
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Direction, type MatrixClient, MatrixEvent, Room } from "../../../src";
import type { MockedObject } from "jest-mock";
import { type MockedObject } from "vitest";
import { Direction, EventType, type MatrixClient, MatrixEvent, Room } from "../../../src";
const CREATOR_USER_ID = "@creator:example.org";
const MODERATOR_USER_ID = "@moderator:example.org";
@@ -23,9 +24,9 @@ const MODERATOR_USER_ID = "@moderator:example.org";
describe("Room", () => {
function createMockClient(): MatrixClient {
return {
supportsThreads: jest.fn().mockReturnValue(true),
decryptEventIfNeeded: jest.fn().mockReturnThis(),
getUserId: jest.fn().mockReturnValue(CREATOR_USER_ID),
supportsThreads: vi.fn().mockReturnValue(true),
decryptEventIfNeeded: vi.fn().mockReturnThis(),
getUserId: vi.fn().mockReturnValue(CREATOR_USER_ID),
} as unknown as MockedObject<MatrixClient>;
}
@@ -129,6 +130,7 @@ describe("Room", () => {
}
}
// eslint-disable-next-line @vitest/expect-expect
it("should apply on ban", async () => {
const { room, messageEvents } = await setupRoom(true);
const banEvent = createRedactOnMembershipChange(CREATOR_USER_ID, MODERATOR_USER_ID, "ban");
@@ -137,6 +139,7 @@ describe("Room", () => {
expectRedacted(messageEvents, room, true);
});
// eslint-disable-next-line @vitest/expect-expect
it("should apply on kick", async () => {
const { room, messageEvents } = await setupRoom(true);
const kickEvent = createRedactOnMembershipChange(CREATOR_USER_ID, MODERATOR_USER_ID, "leave");
@@ -145,6 +148,7 @@ describe("Room", () => {
expectRedacted(messageEvents, room, true);
});
// eslint-disable-next-line @vitest/expect-expect
it("should not apply if the user doesn't have permission to redact", async () => {
const { room, messageEvents } = await setupRoom(false); // difference from other tests here
const banEvent = createRedactOnMembershipChange(CREATOR_USER_ID, MODERATOR_USER_ID, "ban");
@@ -153,6 +157,7 @@ describe("Room", () => {
expectRedacted(messageEvents, room, false);
});
// eslint-disable-next-line @vitest/expect-expect
it("should not apply to self-leaves", async () => {
const { room, messageEvents } = await setupRoom(true);
const leaveEvent = createRedactOnMembershipChange(CREATOR_USER_ID, CREATOR_USER_ID, "leave");
@@ -161,6 +166,7 @@ describe("Room", () => {
expectRedacted(messageEvents, room, false);
});
// eslint-disable-next-line @vitest/expect-expect
it("should not apply to invites", async () => {
const { room, messageEvents } = await setupRoom(true);
const leaveEvent = createRedactOnMembershipChange(CREATOR_USER_ID, CREATOR_USER_ID, "invite");
@@ -169,6 +175,7 @@ describe("Room", () => {
expectRedacted(messageEvents, room, false);
});
// eslint-disable-next-line @vitest/expect-expect
it("should not apply to joins", async () => {
const { room, messageEvents } = await setupRoom(true);
const leaveEvent = createRedactOnMembershipChange(CREATOR_USER_ID, CREATOR_USER_ID, "join");
@@ -177,6 +184,7 @@ describe("Room", () => {
expectRedacted(messageEvents, room, false);
});
// eslint-disable-next-line @vitest/expect-expect
it("should not apply to knocks", async () => {
const { room, messageEvents } = await setupRoom(true);
const leaveEvent = createRedactOnMembershipChange(CREATOR_USER_ID, CREATOR_USER_ID, "knock");
@@ -185,4 +193,71 @@ describe("Room", () => {
expectRedacted(messageEvents, room, false);
});
});
it("should ignore invalid m.room.name events", async () => {
const mockClient = createMockClient();
const room = new Room("!room:example.org", mockClient, CREATOR_USER_ID);
const invalidNameEvent = new MatrixEvent({
type: EventType.RoomName,
content: {
name: { invalid: 123 },
},
state_key: "",
event_id: "$123",
room_id: room.roomId,
sender: CREATOR_USER_ID,
});
// Set up the room
room.currentState.setStateEvents([invalidNameEvent]);
room.recalculate();
expect(room.name).toEqual("Empty room");
});
describe("getAltAliases()", () => {
it("should ignore invalid events", async () => {
const mockClient = createMockClient();
const room = new Room("!room:example.org", mockClient, CREATOR_USER_ID);
const invalidAliasEvent = new MatrixEvent({
type: EventType.RoomCanonicalAlias,
content: {
alt_aliases: [123, "#foo:bar"],
},
state_key: "",
event_id: "$123",
room_id: room.roomId,
sender: CREATOR_USER_ID,
});
// Set up the room
room.currentState.setStateEvents([invalidAliasEvent]);
room.recalculate();
expect(room.getAltAliases()).toEqual(["#foo:bar"]);
});
});
describe("calculateRoomName()", () => {
it("should ignore empty m.room.name 'name' field", async () => {
const mockClient = createMockClient();
const room = new Room("!room:example.org", mockClient, CREATOR_USER_ID);
const event = new MatrixEvent({
type: EventType.RoomName,
content: {
name: "",
},
state_key: "",
event_id: "$123",
room_id: room.roomId,
sender: CREATOR_USER_ID,
});
// Set up the room
room.currentState.setStateEvents([event]);
room.recalculate();
expect(room.name).not.toEqual("");
});
});
});
+35 -37
View File
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { MatrixClient, PendingEventOrdering } from "../../../src/client";
import { Room, RoomEvent } from "../../../src/models/room";
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread";
@@ -43,12 +41,12 @@ describe("Thread", () => {
const myUserId = "@bob:example.org";
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, { timelineSupport: false });
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
client.supportsThreads = vi.fn().mockReturnValue(true);
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "getRoom").mockReturnValue(room);
const { thread } = mkThread({
room,
@@ -86,10 +84,10 @@ describe("Thread", () => {
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
isInitialSyncComplete: jest.fn().mockReturnValue(false),
getRoom: jest.fn().mockImplementation(() => room),
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
supportsThreads: jest.fn().mockReturnValue(true),
isInitialSyncComplete: vi.fn().mockReturnValue(false),
getRoom: vi.fn().mockImplementation(() => room),
decryptEventIfNeeded: vi.fn().mockResolvedValue(void 0),
supportsThreads: vi.fn().mockReturnValue(true),
});
client.reEmitter = mock(ReEmitter, "ReEmitter");
client.canSupport = new Map();
@@ -128,11 +126,11 @@ describe("Thread", () => {
});
room.addReceipt(receipt);
jest.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "getRoom").mockReturnValue(room);
});
afterAll(() => {
jest.resetAllMocks();
vi.resetAllMocks();
});
it("considers own events with no RR as read", () => {
@@ -216,10 +214,10 @@ describe("Thread", () => {
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
isInitialSyncComplete: jest.fn().mockReturnValue(false),
getRoom: jest.fn().mockImplementation(() => room),
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
supportsThreads: jest.fn().mockReturnValue(true),
isInitialSyncComplete: vi.fn().mockReturnValue(false),
getRoom: vi.fn().mockImplementation(() => room),
decryptEventIfNeeded: vi.fn().mockResolvedValue(void 0),
supportsThreads: vi.fn().mockReturnValue(true),
});
client.reEmitter = mock(ReEmitter, "ReEmitter");
client.canSupport = new Map();
@@ -231,11 +229,11 @@ describe("Thread", () => {
room = new Room("123", client, myUserId);
jest.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "getRoom").mockReturnValue(room);
});
afterAll(() => {
jest.resetAllMocks();
vi.resetAllMocks();
});
it("uses unthreaded receipt to figure out read up to", () => {
@@ -319,12 +317,12 @@ describe("Thread", () => {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
client.supportsThreads = vi.fn().mockReturnValue(true);
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "getRoom").mockReturnValue(room);
const { thread } = mkThread({
room,
@@ -336,7 +334,7 @@ describe("Thread", () => {
await emitPromise(thread, ThreadEvent.Update);
expect(thread.length).toBe(2);
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
vi.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
Promise.resolve({
chunk: [],
start: `${token}-new`,
@@ -374,12 +372,12 @@ describe("Thread", () => {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
client.supportsThreads = vi.fn().mockReturnValue(true);
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "getRoom").mockReturnValue(room);
const { thread } = mkThread({
room,
@@ -391,7 +389,7 @@ describe("Thread", () => {
await emitPromise(thread, ThreadEvent.Update);
expect(thread.length).toBe(2);
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
vi.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
Promise.resolve({
chunk: [],
start: `${token}-new`,
@@ -426,12 +424,12 @@ describe("Thread", () => {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
client.supportsThreads = vi.fn().mockReturnValue(true);
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "getRoom").mockReturnValue(room);
const { thread } = mkThread({
room,
@@ -442,7 +440,7 @@ describe("Thread", () => {
});
await emitPromise(thread, ThreadEvent.Update);
expect(thread.length).toBe(2);
const mock = jest.spyOn(thread, "resetLiveTimeline");
const mock = vi.spyOn(thread, "resetLiveTimeline");
mock.mockReturnValue(Promise.resolve());
room.resetLiveTimeline("b1", "f1");
@@ -637,8 +635,8 @@ describe("Thread", () => {
const client = mock(MatrixClient, "MatrixClient");
client.reEmitter = mock(ReEmitter, "ReEmitter");
client.canSupport = canSupport;
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
mocked(client.supportsThreads).mockReturnValue(true);
vi.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
vi.mocked(client.supportsThreads).mockReturnValue(true);
return client;
}
});
@@ -692,7 +690,7 @@ describe("Thread", () => {
// (fetchEditsWhereNeeded only applies to encrypted messages for some reason)
messageToEdit.event.type = EventType.RoomMessageEncrypted;
const editEvent = mkEdit(messageToEdit, client, user, room, "edit");
mocked(client.relations).mockImplementation(async (_roomId, eventId) => {
vi.mocked(client.relations).mockImplementation(async (_roomId, eventId) => {
if (eventId === messageToEdit.getId()) {
return { events: [editEvent] };
} else {
@@ -785,12 +783,12 @@ describe("Thread", () => {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
client.supportsThreads = vi.fn().mockReturnValue(true);
const room = new Room("!room:z", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "getRoom").mockReturnValue(room);
// Create a root event
const rootEvent = mkMessage({
@@ -903,7 +901,7 @@ describe("Thread", () => {
timelineSupport: false,
});
const client = testClient.client;
client.supportsThreads = jest.fn().mockReturnValue(true);
client.supportsThreads = vi.fn().mockReturnValue(true);
// Force server-side support so threads start uninitialised
const prevSupport = Thread.hasServerSideSupport;
@@ -913,7 +911,7 @@ describe("Thread", () => {
const room = new Room("!room:z", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
vi.spyOn(client, "getRoom").mockReturnValue(room);
// Create a root event and thread
const rootEvent = mkMessage({ room: room.roomId, user: myUserId, msg: "Root", event: true });
@@ -1028,13 +1026,13 @@ function createClient(canSupport = new Map()): MatrixClient {
client.reEmitter = mock(ReEmitter, "ReEmitter");
client.canSupport = canSupport;
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
vi.spyOn(client, "supportsThreads").mockReturnValue(true);
vi.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
// Mock methods that call out to HTTP endpoints
jest.spyOn(client, "paginateEventTimeline").mockResolvedValue(true);
jest.spyOn(client, "relations").mockResolvedValue({ events: [] });
jest.spyOn(client, "fetchRoomEvent").mockResolvedValue({});
vi.spyOn(client, "paginateEventTimeline").mockResolvedValue(true);
vi.spyOn(client, "relations").mockResolvedValue({ events: [] });
vi.spyOn(client, "fetchRoomEvent").mockResolvedValue({});
return client;
}
+12 -12
View File
@@ -53,11 +53,11 @@ describe("fixNotificationCountOnDecryption", () => {
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
isInitialSyncComplete: jest.fn().mockReturnValue(false),
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
getRoom: jest.fn().mockImplementation(() => room),
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
supportsThreads: jest.fn().mockReturnValue(true),
isInitialSyncComplete: vi.fn().mockReturnValue(false),
getPushActionsForEvent: vi.fn().mockReturnValue(mkPushAction(true, true)),
getRoom: vi.fn().mockImplementation(() => room),
decryptEventIfNeeded: vi.fn().mockResolvedValue(void 0),
supportsThreads: vi.fn().mockReturnValue(true),
});
mockClient.reEmitter = mock(ReEmitter, "ReEmitter");
mockClient.canSupport = new Map();
@@ -127,8 +127,8 @@ describe("fixNotificationCountOnDecryption", () => {
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
event.getPushActions = vi.fn().mockReturnValue(mkPushAction(false, false));
threadEvent.getPushActions = vi.fn().mockReturnValue(mkPushAction(false, false));
});
it("changes the room count to highlight on decryption", () => {
@@ -200,8 +200,8 @@ describe("fixNotificationCountOnDecryption", () => {
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
event.getPushActions = vi.fn().mockReturnValue(mkPushAction(true, false));
mockClient.getPushActionsForEvent = vi.fn().mockReturnValue(mkPushAction(false, false));
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
@@ -212,8 +212,8 @@ describe("fixNotificationCountOnDecryption", () => {
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
threadEvent.getPushActions = vi.fn().mockReturnValue(mkPushAction(true, false));
mockClient.getPushActionsForEvent = vi.fn().mockReturnValue(mkPushAction(false, false));
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
@@ -221,7 +221,7 @@ describe("fixNotificationCountOnDecryption", () => {
});
it("emits events", () => {
const cb = jest.fn();
const cb = vi.fn();
room.on(RoomEvent.UnreadNotifications, cb);
room.setUnreadNotificationCount(NotificationCountType.Total, 1);

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