Compare commits

..

174 Commits

Author SHA1 Message Date
RiotRobot c622e9260f v25.0.0-rc.1 2023-04-18 11:41:03 +01:00
RiotRobot 8bf53d6f90 Prepare changelog for v25.0.0-rc.1 2023-04-18 11:41:00 +01:00
Michael Telatynski 95f7d1d347 Add typedoc-plugin-mdn-links (#3292) 2023-04-18 10:00:37 +00:00
Michael Telatynski 72d70bb929 Improve types and their safety (#3290)
* Improve types and their safety

* Iterate
2023-04-18 07:32:40 +00:00
Kerry 4f67e59692 Annotate events with executed push rule (#3284)
* unit test paginating /notifications

* add push rule to event

* 1% more test coverage
2023-04-17 21:35:56 +00:00
Kerry d40d5c8a39 unit test paginating /notifications (#3283) 2023-04-16 22:33:52 +00:00
Tulir Asokan 87398ac555 Fix screen sharing on Firefox 113 (#3282)
`getCapabilities` exists now(?), but `setCodecPreferences` doesn't,
which means it would throw an error and fail the call.

Signed-off-by: Tulir Asokan <tulir@maunium.net>
2023-04-15 05:08:21 +00:00
Michael Weimann de3d5ead42 Add Room.threadsTimelineSets doc (#3279)
* Add Room.threadsTimelineSets doc

* Tweak type checks

* Tweak type check again
2023-04-14 09:48:28 +00:00
Richard van der Hoff 1e1b571b28 Expose ServerSideSecretStorage independently of Crypto (#3280)
There is no reason to indirect secret storage via the Crypto layer, and
exposing it directly means it will work for Element-R.

Fixes: https://github.com/vector-im/element-web/issues/24982
2023-04-13 17:21:38 +01:00
Richard van der Hoff f400a7b1b2 Remove eventNames from TypedEventEmitter (#3276) 2023-04-13 10:43:57 +00:00
Enrico Schwendig a0bcb5777f Add Jitter to exported webrtc stats (#3270)
* stats: Add Jitter stats

* Update src/webrtc/stats/trackStatsReporter.ts

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

* stats: Fix typos in tests

* stats: differences between 0 and undefined in jitter val

---------

Co-authored-by: Robin <robin@robin.town>
2023-04-13 08:51:13 +00:00
Richard van der Hoff f8a625eddb OutgoingRoomKeyRequest is only a type (#3278)
Followup to #3275

Fixes a warning from webpack:

    [element-js] WARNING in ../matrix-js-sdk/src/matrix.ts 46:0-61
    [element-js] "export 'OutgoingRoomKeyRequest' was not found in './crypto/store/base'
2023-04-13 07:43:19 +00:00
Richard van der Hoff 69a2a15b95 Add missing methods to ServerSideSecretStorgage (#3277)
(and improve some doc-comments)

Seems like I missed a couple of methods when I created this interface in #3267.
2023-04-13 06:08:53 +00:00
Richard van der Hoff b9d0596dd7 Fix typedoc warnings (#3275)
There aren't that many of these, so I've gone through and fixed them, and
configured the GHA workflow to complain if any creep back in.
2023-04-12 20:30:57 +00:00
Richard van der Hoff 72af8c193c Typescript fix to AccountDataClient (#3274)
* Typescript fix to `AccountDataClient`

obviously, `getAccountDataFromServer` can return null

* Update src/secret-storage.ts

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

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-04-12 18:38:05 +00:00
Richard van der Hoff ed8c326856 Pass UserId into receiveSyncChanges (#3273) 2023-04-12 18:33:12 +00:00
Michael Telatynski f5bf6b1be6 Update types to match reality (#3271) 2023-04-12 17:49:16 +00:00
Richard van der Hoff 0e19f8dc69 Split SecretStorage into two parts (#3267)
* Pull `SecretStorageCallbacks` out of `ICryptoCallbacks`

* Pull the storage part of SecretStorage out to a new class

* Move SecretSharing to a separate class

* Move `ISecretRequest` into `SecretSharing.ts`

* Pull out ISecretStorage interface, and use it

* Mark old `SecretStorage` as deprecated, and rename accesses to it

* Move a `SecretStorage` unit test into its own file

* Use new `SecretStorage` in a couple of places

* add some more unit tests

* Fix test file name

... to match the unit under test

* even more tests

* Add a load of comments

* Rename classes

* Fix some broken tsdoc links

* fix broken test

* Fix compaints about superlinear regex

* just one more test
2023-04-12 16:10:43 +00:00
Richard van der Hoff 6049c0bf37 Improve output in github actions for jest tests (#3269) 2023-04-11 18:53:10 +00:00
texuf a102253f30 Update storage interface so that save() is async to match indexeddb impl (#3221)
The only implementation of this is an async function, but I can’t await it because the interface hides the return type.

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-04-11 18:52:53 +00:00
Florian Duros f71d86f005 Element-R: pass device list change notifications into rust crypto-sdk (#3254) 2023-04-11 14:42:19 +01:00
RiotRobot 70e34ffb76 Resetting package fields for development 2023-04-11 14:00:55 +01:00
RiotRobot 91aa7b26e6 Merge branch 'master' into develop 2023-04-11 14:00:49 +01:00
RiotRobot 7d45947fb3 v24.1.0 2023-04-11 13:59:08 +01:00
RiotRobot 6e174328e8 Prepare changelog for v24.1.0 2023-04-11 13:59:05 +01:00
renovate[bot] 5fb97fcce4 Lock file maintenance (#3268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 09:24:40 +00:00
Hugh Nimmo-Smith 3d1a450129 Export type for return of getCapabilities() (#3266)
* Export type for return of getCapabilities()

Renamed because it clashes with ICapabilities from embedded

* Export type for return of getCapabilities()

Renamed because it clashes with ICapabilities from embedded

* Rename to Capabilities
2023-04-06 12:46:44 +00:00
Michael Telatynski a58c5aacdf Update pull_request.yaml 2023-04-06 12:53:43 +01:00
Michael Weimann d7e165a279 Retry processing potential poll events after decryption (#3246)
* Retry processing potential poll events after decryption

* Point `typedoc` at `matrix.ts`, not `index.ts` (#3239)

This gets rid of the rather pointless "default" module in the generated docs.

* Split up, rename, and move `ISecretStorageKeyInfo` (#3242)

* Move SecretStorageKeyInfo interfaces out to a new module

* Replace usages of ISecretStorageKeyInfo with SecretStorageKeyDescription

* Skip clear text non-poll events

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-04-06 09:59:39 +00:00
renovate[bot] a57ee803f1 Update dependency typescript to v5 (#3262)
* Update dependency typescript to v5

* Update dependency typescript to v5

* Iterate

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-04-06 09:54:02 +00:00
Enrico Schwendig 170a52b09f Measure whether we receive media and add the mute state as an exception (#3249)
* stats: add summery stats reporter

* stats: export summery stats reports

* stats: fix typo of event name

* stats: check promise condition for node 16 test linter

* stats: remove weak test to figure out memory leak

* stats: remove second weak test

* stats: add starting processing test

* stats: fix tests

* stats: fix typo in group call

* stats: fix stats report gathering test

* stats: reactivate promise merge

* stats: add track counter and track mute counter in summary stats

* stats: add summery calculation

* stats: fix PR issues

* stats: adjust summery reporter for inbound and mute state

* stats: check async state

* stats: switch from an `Or` to `And` condition for entire received media value

* stats: Add property description

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2023-04-06 09:32:09 +00:00
renovate[bot] 2be5889d18 Update peter-evans/create-pull-request digest to 38e0b6e (#3257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-06 09:20:40 +00:00
Michael Telatynski ca6b574bee Update upgrade_dependencies.yml (#3264) 2023-04-06 07:51:58 +00:00
Michael Telatynski 57b0172a2d Update pull_request.yaml 2023-04-06 09:17:30 +01:00
Michael Telatynski 53260ee25d Update pull_request.yaml (#3255) 2023-04-06 07:45:44 +00:00
renovate[bot] 964281322f Update all non-major dependencies (#3259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 18:38:55 +00:00
David Baker 7c3f483396 Fix another export type (#3265)
Another case where we were importing types as normal imports which
confuses bundlers.
2023-04-05 17:14:14 +00:00
Hugh Nimmo-Smith 59784aa9fe Support for MSC3882 revision 1 (#3228)
* Support for MSC3882 revision 1

* Additional comments

* Revised field names

* Use UnstableValue for capability
2023-04-05 16:12:29 +00:00
Florian Duros 72a2b6d571 Use processKeyCounts in sliding sync (#3253) 2023-04-05 15:39:46 +00:00
renovate[bot] 5854af0eae Update definitelyTyped (#3260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 15:39:24 +00:00
renovate[bot] acd3d3a804 Update peaceiris/actions-gh-pages digest to 373f7f2 (#3256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 15:23:40 +00:00
renovate[bot] 1b8c04a430 Update babel monorepo (#3258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 15:12:15 +00:00
renovate[bot] 378b73f8b8 Update typescript-eslint monorepo to v5.57.0 (#3261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 15:10:56 +00:00
Michael Telatynski c482a6ab15 Tidy up merge queue automation (#3252)
* Tidy up merge queue automation

* Iterate

* Iterate

* Iterate
2023-04-05 13:48:57 +00:00
Florian Duros 2daa429b77 Improve #3215 implementation (#3226)
* Improve key upload request

* Add fallback keys check

* Review fixes

* Add comments about sliding sync usage of `processKeyCounts`

* Review fixes

* Better wording
2023-04-05 12:35:10 +00:00
Richard van der Hoff 6ebbc15359 Move SecretStorage-related interfaces out to new module (#3244)
* Remove redundant `IAccountDataClient.getAccountData`

This is never called, so we may as well get rid of it

* Move a few more interfaces into `secret-storage.ts`

* Use interfaces from `secret-storage`

* Move IAccountDataClient to secret-storage

* Use `AccountDataClient` from `secret-storage`

* move SECRET_STORAGE_ALGORITHM_V1_AES to secret-storage

* Use `SECRET_STORAGE_ALGORITHM_V1_AES` from `secret-storage`

* Add a test case for the quality gate

* Update src/secret-storage.ts
2023-04-05 11:42:15 +00:00
Richard van der Hoff 9a840d484c Element-R: handle events which arrive before their keys (#3230)
* minor cleanups to the crypto tests

mostly, this is about using `testUtils.awaitDecryption` rather than custom
code. Some other cleanups too.

* Keep a record of events which are missing their keys

* Retry event decryption when we receive megolm keys
2023-04-05 10:00:08 +00:00
David Baker e89467c9fb Add an event emitted when a Call creates a PeerConnection (#3251)
For apps that need acces to the per connection as soon as it's created
for debugging etc.
2023-04-05 08:32:50 +00:00
Enrico Schwendig 0b396c005c indicator whether call members send media. (#3241)
* stats: add summery stats reporter

* stats: export summery stats reports

* stats: fix typo of event name

* stats: check promise condition for node 16 test linter

* stats: remove weak test to figure out memory leak

* stats: remove second weak test

* stats: add starting processing test

* stats: fix tests

* stats: fix typo in group call

* stats: fix stats report gathering test

* stats: reactivate promise merge

* stats: fix PR issues

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2023-04-05 06:48:12 +00:00
David Baker d05313f95e Switch some imports to type imports (#3250)
Having these as regular imports confuses Vite for some reason.
2023-04-04 15:31:16 +00:00
RiotRobot 3f97853011 v24.1.0-rc.1 2023-04-04 11:53:22 +01:00
RiotRobot 2a5c4b1edf Prepare changelog for v24.1.0-rc.1 2023-04-04 11:53:18 +01:00
texuf 65a3c6707c indexddb-local-backend - return the current sync to database promise if a sync is in flight (#3222)
I’m trying to shutdown my matrix clients while using an indexdb, but awaiting the save() function has no effect because a previous sync was in flight. I ended up deleting the matrix client while the save was in flight and I saw a crash.

signed-off-by Austin Ellis <austin@hntlabs.com>

fix linter
2023-04-04 09:36:44 +00:00
Enrico Schwendig 942477f0bf screen share: remove set stream (#3238)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2023-04-04 08:53:32 +00:00
Michael Telatynski 1770b3131a Skip sonarcloud & coverage in merge_queue (#3247) 2023-04-04 08:46:41 +01:00
Richard van der Hoff 41d3ffdab9 Split up, rename, and move ISecretStorageKeyInfo (#3242)
* Move SecretStorageKeyInfo interfaces out to a new module

* Replace usages of ISecretStorageKeyInfo with SecretStorageKeyDescription
2023-04-03 10:11:03 +00:00
Richard van der Hoff 78aa6cb62b Point typedoc at matrix.ts, not index.ts (#3239)
This gets rid of the rather pointless "default" module in the generated docs.
2023-04-03 09:55:11 +00:00
Michael Telatynski b56b8040e6 Use frozen lockfile instead of pure lockfile on yarn install (#3245) 2023-03-31 14:49:49 +00:00
Andy Balaam d1cf98b177 Allow via_servers property in findPredecessor (update to MSC3946) (#3240) 2023-03-31 12:51:10 +00:00
Michael Telatynski 5fc6b3ed17 Fire closed event when IndexedDB closes unexpectedly (#3218)
* Fire `closed` event when IndexedDB closes unexpectedly

* Add test

* Add tests

* Add test

* Add test

* Add test coverage
2023-03-31 08:46:11 +00:00
renovate[bot] 6e41533fed Update jest monorepo to v29.5.0 (#3204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-31 07:52:07 +00:00
David Baker bc76532bd5 Refactor the way group calls hang up (#3234)
* Refactor how group call end calls

We previously used disposeCall to terminate the call which meant that
sometimes a call would never get a hangup event. This changes it so
that we always end a call by calling hangup, then do the cleanup
when the hangup event arrives, so the cleanup is the same whether
we hang up or the other side does.

* Some fixes for failing & hanging tests

* Add type for the call map
2023-03-30 15:57:47 +00:00
Enrico Schwendig 62f1dd79bc Add webrtc stats in full mesh calls (#3232)
* stats: merge stats classes in this branch

* stats: merge call object

* stats: merge export metric events

* stats: fix code style changes

* stats: add missing stats value formatter

* stats: add missing methode to call mock

* stats: add stats for callee

* stats: stop sending stats if call finish

* stats: rename StatsCollector to StatsReportGatherer
2023-03-30 15:17:59 +00:00
David Baker fd3f53e814 Merge pull request #3237 from matrix-org/dbkr/call_events_pass_call_2
Re-apply "Add the call object to Call events"
2023-03-30 10:22:17 +01:00
David Baker 798ac7b94c Revert "Revert "Add the call object to Call events"" 2023-03-29 15:32:25 +01:00
Richard van der Hoff eb0c0f7b93 Element-R: Add support for /discardsession (#3209)
Fixes https://github.com/vector-im/element-web/issues/24431
2023-03-29 13:26:02 +00:00
David Baker f03293f53d Merge pull request #3236 from matrix-org/revert-3229-dbkr/call_events_pass_call
Revert "Add the call object to Call events"
2023-03-29 14:41:59 +01:00
David Baker 7d062387b7 Revert "Add the call object to Call events" 2023-03-29 14:27:49 +01:00
Andy Balaam 9acf3b18ca Update changelog for v24.0.0 now the security issue is public (#3235) 2023-03-29 13:06:32 +00:00
David Baker b41d067c94 Merge pull request #3229 from matrix-org/dbkr/call_events_pass_call
Add the call object to Call events
2023-03-29 12:26:34 +01:00
David Baker c8503b3120 Run prettier on groupCall.ts (#3233) 2023-03-28 15:35:00 +00:00
David Baker da03c3b529 Fix an issue where participants could potentially view video without being displayed themselves
Fix from @robintown, manual merge due to Github having issues.
2023-03-28 15:20:34 +01:00
Robin d48b19e052 Handle group call redaction (#3231)
Redacted group call events should be interpreted as terminated calls.
2023-03-28 13:07:44 +00:00
RiotRobot 6861c67f56 Merge branch 'master' into develop 2023-03-28 14:15:09 +01:00
RiotRobot c87048bd9f v24.0.0 2023-03-28 14:10:53 +01:00
RiotRobot 02269f33b7 Prepare changelog for v24.0.0 2023-03-28 14:10:50 +01:00
Michael Weimann 7f46ae7b97 Further changes for v24.0.0 2023-03-28 14:05:24 +01:00
David Baker 2ab3566f95 Add comment on confusing exception handler I've failed to add a test for 2023-03-28 13:47:22 +01:00
Michael Weimann 9a504af18e Changes for v24.0.0 2023-03-28 11:22:02 +01:00
David Baker 8b50986906 Add test for incoming data channel 2023-03-28 09:30:17 +01:00
David Baker f9a222ecea Mock out media getter after setting up the vocie call
Otherwise we won't get far enough to test the right part
2023-03-27 16:18:00 +01:00
David Baker 5b5a3d8b5e Test failed call upgrade 2023-03-27 15:44:56 +01:00
David Baker 037cbdd214 Basic test for call replace 2023-03-27 14:33:58 +01:00
David Baker 0349411e6d Rename group call error event
So it doesn't clash with the call error event
2023-03-24 14:26:03 +00:00
David Baker d7b75e4b9e Add the call object to Call events
As explained in the comment. I've added it to the end so this should
be completely backwards compatible (although it would be much nicer
if it were the first arg, probably).
2023-03-24 14:09:22 +00:00
Richard van der Hoff 254f043ab0 Enable the prepareToEncrypt test for Element-R (#3225)
This Just Works, so I don't know why I didn't enable the test sooner.
2023-03-24 11:55:17 +00:00
J. Ryan Stinnett 5f3e115545 Stop doing O(n^2) work to find event's home (#3227)
* Stop doing O(n^2) work to find event's home

In certain rooms (e.g. with many state changes hidden via user preferences), the
events array presented to `eventShouldLiveIn` may contain 100s of events. As
part of its various checks, `eventShouldLiveIn` would get an event's associated
ID (reply / relation / redaction parent). It would then use `events.find` to
search the entire (possibly large) `events` array to look for the parent. (This
by itself seems sub-optimal and should probably change to use a map.)

For many events in a room, there is no associated ID. Unfortunately,
`eventShouldLiveIn` did not check whether the associated ID actually exists
before running off to search all of `events`, resulting in O(n^2) work.

This changes `eventShouldLiveIn` to first check that there is an associated ID
before proceeding with its (slow) search. For some rooms, this change
drastically improves performance from ~100% CPU usage to nearly idle.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>

* Add type to `parentEvent`

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

---------

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-03-23 19:02:55 +00:00
Patrick Cloke fc55c4c72a Implement MSC3952: intentional mentions (#3092)
* Add experimental push rules.

* Update for changes to MSC3952: Use event_property_is and event_property_contains.

* Revert custom user/room mention conditions.

* Skip legacy rule processing if mentions exist.

* Add client option for intentional mentions.

* Fix tests.

* Test leagcy behavior with intentional mentions.

* Handle simple review comments.
2023-03-22 20:22:34 +00:00
Florian Duros f795577e14 Send one time key count and unused fallback keys for rust-crypto (#3215)
* Send one time key count and unused fallback keys for rust-crypto

* Add tests

* Remove useless type in promise return

* Add test for one time key upload

* Fix rust-crypto.spec.ts tests

* Remove unneeded code in test

* Add key upload request test

* Fix tests
2023-03-22 10:19:04 +00:00
Eric Eastwood f12cee984a Export TimestampToEventResponse to use in matrix-react-sdk tests (#3223)
* Export ITimestampToEventResponse to use in matrix-react-sdk tests

Part of https://github.com/matrix-org/matrix-react-sdk/pull/10405

* Remove I from interface

As suggested by @weeman1337,
https://github.com/matrix-org/matrix-js-sdk/pull/3223#pullrequestreview-1350612347

See code style guide, https://github.com/vector-im/element-web/blob/50f8be4a623d2280a72920eb1679a4754961f807/code_style.md#typescript--javascript-typescript-javascript

> Interface names should not be marked with an uppercase `I`.
2023-03-21 17:10:28 +00:00
Richard van der Hoff c3b4572841 Add a new test for event encryption, which works with rust (#3203)
* crypto.spec.ts: factor out `expactAliceKeyClaim` utility

* Add a new test for event encryption

... one that actually works on the rust SDK.

* Bump matrix-sdk-crypto-js version

... to pick up recent fixes to race conditions
2023-03-21 16:24:57 +00:00
David Baker 40fe159c10 Merge pull request #3219 from robintown/matrix-widget-api
Update matrix-widget-api
2023-03-21 10:59:50 +00:00
Robin Townsend ddecc87947 Update matrix-widget-api 2023-03-20 10:28:46 -04:00
David Baker 23837266fc Merge pull request #3217 from matrix-org/dbkr/type_sendvoipevent
Add a type to the structure used by the SendVoipEvent event
2023-03-16 10:52:09 +00:00
David Baker 3c9ca8c373 Add a type to the structure used by the SendVoipEvent event 2023-03-15 16:07:10 +00:00
RiotRobot 7f2a4c2568 Resetting package fields for development 2023-03-15 12:41:24 +00:00
RiotRobot 2ad647a73c Merge branch 'master' into develop 2023-03-15 12:41:18 +00:00
RiotRobot 26663e67fd v23.5.0 2023-03-15 12:39:41 +00:00
RiotRobot 0cfc67c679 Prepare changelog for v23.5.0 2023-03-15 12:39:38 +00:00
David Baker 7faba5c2f0 Merge pull request #3213 from matrix-org/dbkr/fix_starting_with_video_muted
Fix bug where video would not unmute if it started muted
2023-03-14 11:18:39 +00:00
Michael Telatynski 1d9250b277 Fix CallOpts types (#3214) 2023-03-14 09:53:59 +00:00
David Baker 08054c1d6d Fix bug where video would not unmute if it started muted
Fixes https://github.com/vector-im/element-call/issues/925
2023-03-13 17:04:43 +00:00
Richard van der Hoff 333872e878 Pull out a public CryptoApi (#3211)
... so that we don't have to implement annoying shims in `MatrixClient`
2023-03-13 12:02:49 +00:00
Janne Mareike Koschinski 913cd257f4 Pin versions of third-party github actions (#3208) 2023-03-10 12:56:23 +00:00
Richard van der Hoff 69f7789c40 Merge pull request #3202 from matrix-org/rav/element-r/encryption_fixes
Fixes to event encryption in the Rust Crypto implementation
2023-03-10 11:00:18 +00:00
renovate[bot] e79ef1f33a Update typescript-eslint monorepo to v5.54.0 (#3198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kerry <kerrya@element.io>
2023-03-10 09:06:58 +00:00
renovate[bot] fb8f61a5ec Update dependency eslint-plugin-unicorn to v46 (#3199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-09 21:28:20 +00:00
Kerry 9b1f2a1d11 adjust beacon model to make beaconInfo definitely assigned in constructor (#3201)
* adjust beacon model to make beaconInfo definitely assigned

* fix

* improvement from PR
2023-03-09 21:03:42 +00:00
Richard van der Hoff 686216fb75 Merge branch 'develop' into rav/element-r/encryption_fixes 2023-03-09 13:34:17 +00:00
Richard van der Hoff f4b83e1a27 Stop a failed /keys/claim request getting stuck
Putting the new request inside a `finally` block meant we would never actually
transition the promise chain from failure to success. Sticking a no-op `catch`
in the chain makes sure that we can recover from an error.
2023-03-09 10:54:50 +00:00
Richard van der Hoff 97f21b6635 Send the outgoing m.room_key messages returned by shareRoomKey
I forgot this in https://github.com/matrix-org/matrix-js-sdk/pull/3122 :(.

To be honest, I'm not sure how it ever worked.
2023-03-09 10:50:49 +00:00
Michael Telatynski 87641a6803 Improve processBeaconEvents hotpath (#3200)
* Attempt at improving beacons hotpath

* Iterate and fix tests
2023-03-09 09:24:57 +00:00
renovate[bot] 7e4331172a chore(deps): update all non-major dependencies (#3197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-08 15:33:44 +00:00
renovate[bot] a976080d1b chore(deps): update dependency @types/node to v18.14.6 (#3196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-08 08:16:55 +00:00
Patrick Cloke bcf3bba44e Implement MSC3966: a push rule condition to check if an array contains a value (#3180)
* Support MSC3966 to match values in an array in push rule conditions.

* Update to stable identifiers.

* Appease the linter.
2023-03-07 16:36:06 +00:00
Michael Telatynski 54ac36d424 Revert "Add GHA for requiring all reviews to be dismissed or approving before merging (#2391)" (#3195)
This reverts commit e84c90dbbc.
2023-03-07 15:21:20 +00:00
Michael Telatynski e84c90dbbc Add GHA for requiring all reviews to be dismissed or approving before merging (#2391)
* Add GHA for requiring all reviews to be dismissed or approving before merging

* Attempt again

* Try try try again

* stash

* Stash

* Dummy

* Fix indentation

* Iterate

* Tweak

* Iterate

* toJSON

* Iterate

* Iterate

* Fix

* typo

* Permissions

* tidy

* Update pending_reviews.yml

* delint
2023-03-07 14:23:56 +00:00
Michael Telatynski 4424438658 Fix jest/no-conditional-expect lint and enable it (#3194) 2023-03-07 12:44:03 +00:00
RiotRobot 13d95c8219 v23.5.0-rc.1 2023-03-07 11:19:25 +00:00
RiotRobot e119dc4e89 Prepare changelog for v23.5.0-rc.1 2023-03-07 11:19:22 +00:00
Patrick Cloke b4cdc5a923 Implement MSC3758: a push rule condition to match event properties exactly (#3179)
* Add some comments.

* Support MSC3758 to exactly match values in push rule conditions.

* Update to stable prefix.
2023-03-06 14:52:43 +00:00
Andy Balaam a82e22b5de Remove items incorrectly included in changelog for 23.4.0 (#3190) 2023-03-03 13:30:38 +00:00
Patrick Cloke c894d09d8c Room call is an underride, not an override. (#3185) 2023-03-03 13:30:28 +00:00
Richard van der Hoff 585ce07260 Stub out the rust crypto implementation for browserify (#3187)
Fixes #3182.
2023-03-03 10:18:45 +00:00
Enrico Schwendig 8cbbdaa239 groupCall: make no media call param optional (#3186)
- ensure group call backwards-compatibility and move `allowCallWithoutVideoAndAudio`to the end of the `CroupCall` constructor
2023-03-02 18:55:09 +00:00
Damir Jelić cd526a254d Stop requesting room keys from other users (#2982)
* Refactor the room key handling method

* Fix the forwarded room key test to use the same user ids.

We have some tests that check if receiving a forwarded room key works.
These claim to use the same user id, but in fact they change the user id
in the last moment before the event is passed into the client.

Let's change this so we're always operating with the same user id.

* Stop requesting room keys from other users

We never accept such room keys, so there isn't a point in requesting
them.

* fixup! Refactor the room key handling method

* Apply suggestions from code review

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

* fixup! Refactor the room key handling method

* fixup! Apply suggestions from code review

* fixup! Refactor the room key handling method

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-03-02 16:38:24 +00:00
Enrico Schwendig e782a2afa3 Enable group calls without video and audio track by configuration of MatrixClient (#3162)
* groupCall: add configuration param to allow no audio and no camera

* groupCall: enable datachannel to do no media group calls

* groupCall: changed call no media property as object property

* groupCall: fix existing unit tests

* groupCall: remove not needed flag

* groupCall: rename property to allow no media calls

* groupCall: mute unmute even without device

* groupCall: switch to promise callbacks

* groupCall: switch to try catch

* test: filter dummy code from coverage

* test: extend media mute tests

* groupCall: move permission check to device handler

* mediaHandler: add error in log statement
2023-03-02 16:35:52 +00:00
Hugh Nimmo-Smith 565339b1fd Remove experimental support for MSC3903 v1 (#3184)
* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Reinstate v1 support to make this a non-breaking change

Deprecates several experimental types

* Remove MSC3903 v1 support

This is a breaking change in code marked unstable/experimental

Revert "Reinstate v1 support to make this a non-breaking change"

This reverts commit 89773458b9a1e5f332938e5574f35b16d204d75d.
2023-03-02 16:34:22 +00:00
Hugh Nimmo-Smith 493203050a Support for v2 of MSC3903 (#3155)
* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Test correct protocol version

* Fix up test

* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Test correct protocol version

* Fix up test

* Reinstate v1 support to make this a non-breaking change

Deprecates several experimental types
2023-03-02 13:15:17 +00:00
renovate[bot] 41782c4593 Update dependency eslint-plugin-jsdoc to v40 (#3173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 07:23:55 +00:00
renovate[bot] 86256a4e74 Update definitelyTyped (#3170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 07:23:42 +00:00
Kerry 933a0c9909 Polls push rules (#3181)
* add poll push rule ids

* add getPushRuleAndKindById method to pushprocessor
2023-03-01 20:30:40 +00:00
Patrick Cloke c8a4d9b88a Implement MSC3873 to handle escaped dots in push rule keys (#3134)
* Add comments.

* Implment MSC3873 to handle escaped dots in keys.

* Add some comments about tests.

* Clarify spec behavior.

* Fix typo.

* Don't manually iterate string.

* Clean-up tests.

* Simplify tests.

* Add more tests & fix bug with empty parts.

* Add more edge cases.

* Add a regular expression solution.

This is ~80% slower than the basic split(".").

* Split on a simpler regular expression.

This is ~50% slower than a simple split(".").

* Remove redundant case in regex.

* Enable sticky regex.

* Rollback use of regex.

* Cache values in the PushProcessor.

* Use more each in tests.

* Pre-calculate the key parts instead of caching them.

* Fix typo.

* Switch back to external cache, but clean out obsolete cached values.

* Remove obsolete property.

* Remove more obsolete properties.
2023-03-01 12:23:40 +00:00
RiotRobot 437128d11b Resetting package fields for development 2023-02-28 10:37:23 +00:00
RiotRobot c18d09fd22 Merge branch 'master' into develop 2023-02-28 10:36:51 +00:00
RiotRobot 2a5e5e6a59 v23.4.0 2023-02-28 10:25:34 +00:00
RiotRobot 68e354752b Prepare changelog for v23.4.0 2023-02-28 10:25:31 +00:00
Michael Telatyński d80b7499fd Fix spec compliance issue around encrypted m.relates_to (#3178)
* Fix spec compliance issue around encrypted `m.relates_to`

* Add test
2023-02-27 22:12:45 +00:00
Janne Mareike Koschinski 9c8093eb3e Fix reactions in threads sometimes causing stuck notifications (#3146)
* Associate event with thread before adding it to the thread timeline

* Make sure events can be added to thread correctly

* Write initial test case

* Add additional comment for why the code had to be reordered
2023-02-24 13:12:06 +00:00
renovate[bot] aec1c11037 Update all non-major dependencies (#3171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-24 12:17:19 +00:00
renovate[bot] e2e9986059 Update babel monorepo to v7.21.0 (#3177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-23 16:43:01 +00:00
Michael Telatynski d70ffdbc02 Improve types (#3175)
* Improve types

* Add test
2023-02-22 17:39:37 +00:00
RiotRobot 0f1f1db3d2 v23.4.0-rc.2 2023-02-22 11:07:38 +00:00
RiotRobot 77b91a45cb Prepare changelog for v23.4.0-rc.2 2023-02-22 11:07:35 +00:00
RiotRobot fe6add9396 Merge branch 'develop' into staging 2023-02-22 10:59:21 +00:00
renovate[bot] 21cc9c3d8a Update jest monorepo to v29.4.3 (#3169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 10:23:29 +00:00
renovate[bot] 2d59c4647d Update typescript-eslint monorepo to v5.52.0 (#3172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 10:19:53 +00:00
Florian Duros 1f0c6a6dc9 Add easy way to determine if the decryption failure is due to "DecryptionError: The sender has disabled encrypting to unverified devices." (#3167)
* Add isEncryptedDisabledForUnverifiedDevices in event.ts

* Add Tests

* Add isEncryptedDisabledForUnverifiedDevices properties to event

* Use WITHHELD_MESSAGES instead of hardcoded string

* Use getter instead of function

* Add documentation
2023-02-21 16:13:43 +00:00
RiotRobot c9b502fb0e v23.4.0-rc.1 2023-02-21 11:56:15 +00:00
RiotRobot 330fbaccfc Prepare changelog for v23.4.0-rc.1 2023-02-21 11:56:12 +00:00
Michael Telatynski 937f370655 Run matrix-react-sdk tests on merge queue (#3161)
* First attempt at merge queues for downstream testing

* Debug

* delint

* Fix typo

* Test

* Iterate

* Fix checkout

* rerun

* experiment

* Iterate

* Iterate

* iterate

* Iterate

* Finalise

* Disable coverage for downstream tests

* Update tests.yml

* Apply merge_queue trigger to other CI

* delint

* delint

* Add exception
2023-02-21 11:09:23 +00:00
Kerry a8ad3ed26d Polls: expose end event id on poll model (#3160) 2023-02-20 10:30:19 +00:00
Michael Telatynski decac58a18 Better type guard parseTopicContent (#3165) 2023-02-20 10:29:33 +00:00
Kerry 1a91ba59a6 Polls: count undecryptable poll relations (#3163) 2023-02-20 10:10:38 +00:00
Michael Telatynski 89df43a975 Fix predecessor types, nowhere does the spec say it can be null (#3159)
* Fix predecessor types, nowhere does the spec say it can be `null`

* Iterate

* Update comment

* update test
2023-02-16 09:38:36 +00:00
Michael Telatynski ad98706db4 Iterate types (#3156) 2023-02-15 13:38:41 +00:00
Will Hunt 195d1730bd Fix notification counts for encrypted rooms with ignored event rules (#3130)
* Validate vars early

* Split out unread  counts for total and highlight to different logic blocks

* Add tests for ignoring non notifying events

* Fix possibly incorrect tests?

* lint fix

* Refactor currentTotalCount

* Track Total locally too

* Lots of total count assumptions and comments

* Adjust for threading too

* Fixup tests

* a word

* lint fix
2023-02-15 11:25:13 +00:00
Richard van der Hoff db4bd907f8 Switch crypto.spec.ts away from TestClient and matrix-mock-request. (#3142)
I became sufficiently annoyed with matrix-mock-request that I decided to replace it with fetch-mock, which is what we use in matrix-react-sdk and is generally more powerful, easier to use, and actually maintained.

Unfortunately, we have a TestClient utility which is widely used and quite tightly integrated with matrix-mock-request. It wasn't going to be possible to change TestClient without changing all the tests that use it.

I also don't find TestClient particularly easy to use - it does a lot of stuff which I'm not convinced ought to be done for every single test.

So... I've introduced a couple of new classes (SyncResponder, E2EKeyReceiver) which do some of the useful bits of TestClient, but in a more granular way, and have switched crypto.spec.ts over so that rather than instantiating a TestClient for each test, it creates a MatrixClient directly and intercepts the endpoints necessary.
2023-02-15 10:39:24 +00:00
Kerry cdd7dbbb2b decrypt poll relations before processing (#3148) 2023-02-14 21:49:52 +00:00
RiotRobot 108f157324 Resetting package fields for development 2023-02-14 10:23:07 +00:00
RiotRobot 8da39ec8f4 Merge branch 'master' into develop 2023-02-14 10:23:02 +00:00
Andy Balaam 5e17626fe0 Include 'browser' in list of adjusted properties in release.sh (#3149) 2023-02-14 09:50:55 +00:00
David Baker abc9c9dcb0 Merge pull request #3147 from matrix-org/dbkr/stop_ice_timer_on_terminate
Stop the ICE disconnected timer on call terminate
2023-02-13 17:35:45 +01:00
David Baker f346fcb056 Stop the ICE disconnected timer on call terminate
It just wasn't getting stopped, so if the call ended while ICE was
disconnected, we'd get confusing error messages after the call ended.
2023-02-13 16:15:57 +00:00
Eric Eastwood c67325ba07 Add matrix-org/jest linting (#2973) 2023-02-10 12:05:40 +01:00
renovate[bot] a063ae8ce7 Update jest monorepo to v29.4.1 (#3133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 15:44:23 +00:00
renovate[bot] f9e5535492 Update typescript-eslint monorepo to v5.50.0 (#3136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 16:29:03 +01:00
renovate[bot] 015d9c5c4f Update dependency @types/node to v18.11.19 (#3137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 16:28:22 +01:00
Germain b6d40078d9 Clear notifications when we can infer read status from receipts (#3139) 2023-02-09 10:18:18 +00:00
David Baker b8a8f4850a Merge pull request #3123 from matrix-org/SimonBrandner/task/logging
Improve WebRTC logging
2023-02-08 17:10:40 +00:00
Šimon Brandner 1cc23d789c Add new tests to groupCall
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-02-08 17:24:15 +01:00
Janne Mareike Koschinski 5cf0bb46a4 Messages sent out of order after one message fails (#3131)
* Instead of skipping, bail out by clearing queue
* Allow additional status transition for events from QUEUED to NOT_SENT
2023-02-08 13:23:30 +01:00
renovate[bot] 16672b3d0c Update all non-major dependencies (#3132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-07 17:36:38 +00:00
Šimon Brandner 31459a5d63 Improve logging
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-02-03 11:40:48 +01:00
177 changed files with 12097 additions and 4347 deletions
+1 -1
View File
@@ -23,4 +23,4 @@ indent_size = 4
trim_trailing_whitespace = true
[*.{yml,yaml}]
indent_size = 2
indent_size = 4
+15 -1
View File
@@ -1,6 +1,6 @@
module.exports = {
plugins: ["matrix-org", "import", "jsdoc"],
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
parserOptions: {
project: ["./tsconfig.json"],
},
@@ -63,6 +63,20 @@ module.exports = {
],
},
],
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"jest/no-disabled-tests": "off",
// TODO: There are many tests with invalid expects that should be fixed,
// https://github.com/matrix-org/matrix-js-sdk/issues/2976
"jest/valid-expect": "off",
// Also treat "oldBackendOnly" as a test function.
// Used in some crypto tests.
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
},
],
},
overrides: [
{
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
)
)
steps:
- uses: tibdex/backport@v2
- uses: tibdex/backport@2e217641d82d02ba0603f46b1aeedefb258890ac # v2
with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact
uses: dawidd6/action-download-artifact@bd10f381a96414ce2b13a11bfa89902ba7cea07f # v2.24.3
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2
with:
workflow: static_analysis.yml
run_id: ${{ github.event.workflow_run.id }}
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@v2
uses: peter-evans/repository-dispatch@26b39ed245ab8f31526069329e112ab2fb224588 # v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
+5 -8
View File
@@ -2,23 +2,20 @@ name: Pull Request
on:
pull_request_target:
types: [opened, edited, labeled, unlabeled, synchronize]
merge_group:
types: [checks_requested]
workflow_call:
inputs:
labels:
type: string
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
required: false
description: "No longer used, uses allchange logic now, will be removed at a later date"
secrets:
ELEMENT_BOT_TOKEN:
required: true
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
jobs:
changelog:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
if: github.event_name != 'merge_group'
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
requireLabel: true
@@ -42,7 +39,7 @@ jobs:
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@v2
uses: tspascoal/get-user-teams-membership@37c08f7b52a72ca95d12af2e7ab2553ca9adf13b # v2
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
+2 -2
View File
@@ -20,11 +20,11 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
run: "yarn install --frozen-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@v1
uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
cache: "yarn"
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
run: "yarn install --frozen-lockfile"
- name: 📖 Generate JSDoc
run: "yarn gendoc"
@@ -46,7 +46,7 @@ jobs:
fi
- name: 🚀 Deploy
uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
keep_files: true
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@v1
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
@@ -42,7 +42,7 @@ jobs:
coverage_extract_path: coverage
extra_args: ${{ inputs.extra_args }}
- uses: Sibz/github-status-action@v1
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
+3 -1
View File
@@ -11,6 +11,7 @@ jobs:
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
prepare:
name: Prepare
if: github.event.workflow_run.event != 'merge_group'
runs-on: ubuntu-latest
outputs:
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
@@ -19,7 +20,7 @@ jobs:
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact
uses: dawidd6/action-download-artifact@v2
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2
with:
workflow: tests.yaml
run_id: ${{ github.event.workflow_run.id }}
@@ -35,6 +36,7 @@ jobs:
sonarqube:
name: 🩻 SonarQube
if: github.event.workflow_run.event != 'merge_group'
needs: prepare
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
+3 -1
View File
@@ -1,6 +1,8 @@
name: Static Analysis
on:
pull_request: {}
merge_group:
types: [checks_requested]
push:
branches: [develop, master]
concurrency:
@@ -63,7 +65,7 @@ jobs:
run: "yarn install"
- name: Generate Docs
run: "yarn run gendoc"
run: "yarn run gendoc --treatWarningsAsErrors"
- name: Upload Artifact
uses: actions/upload-artifact@v3
+46 -12
View File
@@ -1,11 +1,15 @@
name: Tests
on:
pull_request: {}
merge_group:
types: [checks_requested]
push:
branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
jobs:
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})"
@@ -34,28 +38,58 @@ jobs:
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1
uses: SimenB/github-actions-cpu-cores@410541432439795d30db6501fb1d8178eb41e502 # v1
- name: Run tests with coverage and metrics
if: github.ref == 'refs/heads/develop'
- name: Run tests
run: |
yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/spec/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
yarn test \
--coverage=${{ env.ENABLE_COVERAGE }} \
--ci \
--max-workers ${{ steps.cpu-cores.outputs.count }} \
./spec/${{ matrix.specs }}
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
- name: Run tests with coverage
if: github.ref != 'refs/heads/develop'
run: |
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
- name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true'
run: mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
coverage
!coverage/lcov-report
matrix-react-sdk:
name: Downstream test matrix-react-sdk
if: github.event_name == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
with:
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
# Hook for branch protection to skip downstream testing outside of merge queues
# and skip sonarcloud coverage within merge queues
downstream:
name: Downstream tests
runs-on: ubuntu-latest
if: always()
needs:
- matrix-react-sdk
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
description: SonarCloud skipped
context: SonarCloud Code Analysis
sha: ${{ github.sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
run: exit 1
+5 -5
View File
@@ -20,7 +20,7 @@ jobs:
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/upgrade-deps
@@ -31,8 +31,8 @@ jobs:
T-Task
- name: Enable automerge
uses: peter-evans/enable-pull-request-automerge@v2
run: gh pr merge --merge --auto "$PR_NUMBER"
if: steps.cpr.outputs.pull-request-operation == 'created'
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
env:
GH_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
+1
View File
@@ -19,3 +19,4 @@ out
.vscode
.vscode/
.idea/
+70
View File
@@ -1,3 +1,73 @@
Changes in [25.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.0.0-rc.1) (2023-04-18)
============================================================================================================
## 🚨 BREAKING CHANGES
* Change `Store.save()` to return a `Promise` ([\#3221](https://github.com/matrix-org/matrix-js-sdk/pull/3221)). Contributed by @texuf.
## ✨ Features
* Add typedoc-plugin-mdn-links ([\#3292](https://github.com/matrix-org/matrix-js-sdk/pull/3292)).
* Annotate events with executed push rule ([\#3284](https://github.com/matrix-org/matrix-js-sdk/pull/3284)). Contributed by @kerryarchibald.
* Element-R: pass device list change notifications into rust crypto-sdk ([\#3254](https://github.com/matrix-org/matrix-js-sdk/pull/3254)). Fixes vector-im/element-web#24795. Contributed by @florianduros.
* Support for MSC3882 revision 1 ([\#3228](https://github.com/matrix-org/matrix-js-sdk/pull/3228)). Contributed by @hughns.
## 🐛 Bug Fixes
* Fix screen sharing on Firefox 113 ([\#3282](https://github.com/matrix-org/matrix-js-sdk/pull/3282)). Contributed by @tulir.
* Retry processing potential poll events after decryption ([\#3246](https://github.com/matrix-org/matrix-js-sdk/pull/3246)). Fixes vector-im/element-web#24568.
* Element-R: handle events which arrive before their keys ([\#3230](https://github.com/matrix-org/matrix-js-sdk/pull/3230)). Fixes vector-im/element-web#24489.
Changes in [24.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.1.0) (2023-04-11)
==================================================================================================
## ✨ Features
* Allow via_servers property in findPredecessor (update to MSC3946) ([\#3240](https://github.com/matrix-org/matrix-js-sdk/pull/3240)). Contributed by @andybalaam.
* Fire `closed` event when IndexedDB closes unexpectedly ([\#3218](https://github.com/matrix-org/matrix-js-sdk/pull/3218)).
* Implement MSC3952: intentional mentions ([\#3092](https://github.com/matrix-org/matrix-js-sdk/pull/3092)). Fixes vector-im/element-web#24376.
* Send one time key count and unused fallback keys for rust-crypto ([\#3215](https://github.com/matrix-org/matrix-js-sdk/pull/3215)). Fixes vector-im/element-web#24795. Contributed by @florianduros.
* Improve `processBeaconEvents` hotpath ([\#3200](https://github.com/matrix-org/matrix-js-sdk/pull/3200)).
* Implement MSC3966: a push rule condition to check if an array contains a value ([\#3180](https://github.com/matrix-org/matrix-js-sdk/pull/3180)).
## 🐛 Bug Fixes
* indexddb-local-backend - return the current sync to database promise … ([\#3222](https://github.com/matrix-org/matrix-js-sdk/pull/3222)). Contributed by @texuf.
* Revert "Add the call object to Call events" ([\#3236](https://github.com/matrix-org/matrix-js-sdk/pull/3236)).
* Handle group call redaction ([\#3231](https://github.com/matrix-org/matrix-js-sdk/pull/3231)). Fixes vector-im/voip-internal#128.
* Stop doing O(n^2) work to find event's home (`eventShouldLiveIn`) ([\#3227](https://github.com/matrix-org/matrix-js-sdk/pull/3227)). Contributed by @jryans.
* Fix bug where video would not unmute if it started muted ([\#3213](https://github.com/matrix-org/matrix-js-sdk/pull/3213)). Fixes vector-im/element-call#925.
* Fixes to event encryption in the Rust Crypto implementation ([\#3202](https://github.com/matrix-org/matrix-js-sdk/pull/3202)).
Changes in [24.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.0.0) (2023-03-28)
==================================================================================================
## 🔒 Security
* Fixes for [CVE-2023-28427](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-28427) / GHSA-mwq8-fjpf-c2gr
Changes in [23.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.5.0) (2023-03-15)
==================================================================================================
## ✨ Features
* Implement MSC3758: a push rule condition to match event properties exactly ([\#3179](https://github.com/matrix-org/matrix-js-sdk/pull/3179)).
* Enable group calls without video and audio track by configuration of MatrixClient ([\#3162](https://github.com/matrix-org/matrix-js-sdk/pull/3162)). Contributed by @EnricoSchw.
* Updates to protocol used for Sign in with QR code ([\#3155](https://github.com/matrix-org/matrix-js-sdk/pull/3155)). Contributed by @hughns.
* Implement MSC3873 to handle escaped dots in push rule keys ([\#3134](https://github.com/matrix-org/matrix-js-sdk/pull/3134)). Fixes undefined/matrix-js-sdk#1454.
## 🐛 Bug Fixes
* Fix spec compliance issue around encrypted `m.relates_to` ([\#3178](https://github.com/matrix-org/matrix-js-sdk/pull/3178)).
* Fix reactions in threads sometimes causing stuck notifications ([\#3146](https://github.com/matrix-org/matrix-js-sdk/pull/3146)). Fixes vector-im/element-web#24000. Contributed by @justjanne.
Changes in [23.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0) (2023-02-28)
==================================================================================================
## ✨ Features
* Add easy way to determine if the decryption failure is due to "DecryptionError: The sender has disabled encrypting to unverified devices." ([\#3167](https://github.com/matrix-org/matrix-js-sdk/pull/3167)). Contributed by @florianduros.
* Polls: expose end event id on poll model ([\#3160](https://github.com/matrix-org/matrix-js-sdk/pull/3160)). Contributed by @kerryarchibald.
* Polls: count undecryptable poll relations ([\#3163](https://github.com/matrix-org/matrix-js-sdk/pull/3163)). Contributed by @kerryarchibald.
## 🐛 Bug Fixes
* Better type guard parseTopicContent ([\#3165](https://github.com/matrix-org/matrix-js-sdk/pull/3165)). Fixes matrix-org/element-web-rageshakes#20177 and matrix-org/element-web-rageshakes#20178.
* Fix a bug where events in encrypted rooms would sometimes erroneously increment the total unread counter after being processed locally. ([\#3130](https://github.com/matrix-org/matrix-js-sdk/pull/3130)). Fixes vector-im/element-web#24448. Contributed by @Half-Shot.
* Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)).
* Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991.
* Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne.
Changes in [23.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.3.0) (2023-02-14)
==================================================================================================
+39
View File
@@ -0,0 +1,39 @@
/* 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",
};
// if we're running under GHA, enable the GHA reporter
if (env["GITHUB_ACTIONS"] !== undefined) {
const reporters: Config["reporters"] = [["github-actions", { silent: false }], "summary"];
// 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.js");
}
config.reporters = reporters;
}
export default config;
+36 -27
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "23.3.0",
"version": "25.0.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
@@ -14,7 +14,7 @@
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdir dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:compile-browser": "mkdir dist && BROWSERIFYSWAP_ENV='no-rust-crypto' browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
@@ -55,13 +55,13 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.6",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.0.0",
"matrix-widget-api": "^1.3.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
@@ -84,6 +84,7 @@
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
"@types/domexception": "^4.0.0",
"@types/jest": "^29.0.0",
"@types/node": "18",
@@ -96,17 +97,20 @@
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"browserify-swap": "^0.2.2",
"debug": "^4.3.4",
"docdash": "^2.0.0",
"domexception": "^4.0.0",
"eslint": "8.32.0",
"eslint": "8.37.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.6.4",
"eslint-plugin-matrix-org": "^0.10.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-jsdoc": "^40.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^45.0.0",
"eslint-plugin-unicorn": "^46.0.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"fetch-mock-jest": "^1.5.1",
@@ -115,35 +119,40 @@
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"matrix-mock-request": "^2.5.0",
"prettier": "2.8.3",
"prettier": "2.8.7",
"rimraf": "^4.0.0",
"terser": "^5.5.1",
"ts-node": "^10.9.1",
"tsify": "^5.0.2",
"typedoc": "^0.23.20",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^1.0.0",
"typescript": "^4.5.3"
},
"jest": {
"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"
"typescript": "^5.0.0"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"browserify": {
"transform": [
"browserify-swap",
[
"babelify",
{
"sourceMaps": "inline",
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
}
]
]
},
"browserify-swap": {
"no-rust-crypto": {
"src/rust-crypto/index.ts$": "./src/rust-crypto/browserify-index.ts"
}
},
"typings": "./lib/index.d.ts"
}
+2 -2
View File
@@ -130,7 +130,7 @@ fi
# global cache here to ensure we get the right thing.
yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts --pure-lockfile
yarn install --ignore-scripts --frozen-lockfile
# ignore leading v on release
release="${1#v}"
@@ -225,7 +225,7 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
yarn install --pure-lockfile
yarn install --frozen-lockfile
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" yarn dist
+25 -2
View File
@@ -16,11 +16,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
/* eslint-disable jest/no-standalone-expect */
// load olm before the sdk if possible
import "./olm-loader";
import MockHttpBackend from "matrix-mock-request";
import type { IDeviceKeys, IOneTimeKey } from "../src/@types/crypto";
import type { IE2EKeyReceiver } from "./test-utils/E2EKeyReceiver";
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
import { logger } from "../src/logger";
import { syncPromise } from "./test-utils/test-utils";
@@ -28,14 +33,18 @@ import { createClient, IStartClientOpts } from "../src/matrix";
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils";
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
import { IKeyBackupSession } from "../src/crypto/keybackup";
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
import { 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
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
*/
export class TestClient {
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
public readonly httpBackend: MockHttpBackend;
public readonly client: MatrixClient;
public deviceKeys?: IDeviceKeys | null;
@@ -240,8 +249,22 @@ export class TestClient {
return this.deviceKeys!.keys[keyId];
}
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
*
* Calling this will register a response for `/sync`, and then, in the background, flush a single `/sync` request.
* Try calling {@link syncPromise} to wait for the sync to complete.
*
* @param response - response to /sync request
*/
public sendOrQueueSyncResponse(syncResponse: object): void {
this.httpBackend.when("GET", "/sync").respond(200, syncResponse);
this.httpBackend.flush("/sync", 1);
}
/**
* flush a single /sync request, and wait for the syncing event
*
* @deprecated: prefer to use {@link #sendOrQueueSyncResponse} followed by {@link syncPromise}.
*/
public flushSync(): Promise<void> {
logger.log(`${this}: flushSync`);
+683 -479
View File
File diff suppressed because it is too large Load Diff
@@ -175,7 +175,7 @@ describe("MatrixClient events", function () {
});
});
it("should emit User events", function (done) {
it("should emit User events", async () => {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let fired = false;
@@ -192,10 +192,8 @@ describe("MatrixClient events", function () {
});
client!.startClient();
httpBackend!.flushAllExpected().then(function () {
expect(fired).toBe(true);
done();
});
await httpBackend!.flushAllExpected();
expect(fired).toBe(true);
});
it("should emit Room events", function () {
+57 -35
View File
@@ -205,19 +205,17 @@ describe("MatrixClient", function () {
describe("getFilter", function () {
const filterId = "f1lt3r1d";
it("should return a filter from the store if allowCached", function (done) {
it("should return a filter from the store if allowCached", async () => {
const filter = Filter.fromJson(userId, filterId, {
event_format: "client",
});
store!.storeFilter(filter);
client!.getFilter(userId, filterId, true).then(function (gotFilter) {
expect(gotFilter).toEqual(filter);
done();
});
const gotFilter = await client!.getFilter(userId, filterId, true);
expect(gotFilter).toEqual(filter);
httpBackend!.verifyNoOutstandingRequests();
});
it("should do an HTTP request if !allowCached even if one exists", function (done) {
it("should do an HTTP request if !allowCached even if one exists", async () => {
const httpFilterDefinition = {
event_format: "federation",
};
@@ -230,15 +228,11 @@ describe("MatrixClient", function () {
event_format: "client",
});
store!.storeFilter(storeFilter);
client!.getFilter(userId, filterId, false).then(function (gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
done();
});
httpBackend!.flush("");
const [gotFilter] = await Promise.all([client!.getFilter(userId, filterId, false), httpBackend!.flush("")]);
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
});
it("should do an HTTP request if nothing is in the cache and then store it", function (done) {
it("should do an HTTP request if nothing is in the cache and then store it", async () => {
const httpFilterDefinition = {
event_format: "federation",
};
@@ -247,20 +241,16 @@ describe("MatrixClient", function () {
httpBackend!
.when("GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId)
.respond(200, httpFilterDefinition);
client!.getFilter(userId, filterId, true).then(function (gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store!.getFilter(userId, filterId)).toBeTruthy();
done();
});
httpBackend!.flush("");
const [gotFilter] = await Promise.all([client!.getFilter(userId, filterId, true), httpBackend!.flush("")]);
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store!.getFilter(userId, filterId)).toBeTruthy();
});
});
describe("createFilter", function () {
const filterId = "f1llllllerid";
it("should do an HTTP request and then store the filter", function (done) {
it("should do an HTTP request and then store the filter", async () => {
expect(store!.getFilter(userId, filterId)).toBe(null);
const filterDefinition = {
@@ -276,13 +266,9 @@ describe("MatrixClient", function () {
filter_id: filterId,
});
client!.createFilter(filterDefinition).then(function (gotFilter) {
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
expect(store!.getFilter(userId, filterId)).toEqual(gotFilter);
done();
});
httpBackend!.flush("");
const [gotFilter] = await Promise.all([client!.createFilter(filterDefinition), httpBackend!.flush("")]);
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
expect(store!.getFilter(userId, filterId)).toEqual(gotFilter);
});
});
@@ -617,14 +603,14 @@ describe("MatrixClient", function () {
});
const prom = client!.downloadKeys(["boris", "chaz"]).then(function (res) {
assertObjectContains(res.boris.dev1, {
assertObjectContains(res.get("boris")!.get("dev1")!, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { abc: "def" },
});
assertObjectContains(res.chaz.dev2, {
assertObjectContains(res.get("chaz")!.get("dev2")!, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2": ed25519key },
algorithms: ["2"],
@@ -1141,22 +1127,51 @@ describe("MatrixClient", function () {
describe("requestLoginToken", () => {
it("should hit the expected API endpoint with UIA", async () => {
httpBackend!
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
const response = {};
const uiaData = {};
const prom = client!.requestLoginToken(uiaData);
httpBackend!
.when("POST", "/unstable/org.matrix.msc3882/login/token", { auth: uiaData })
.when("POST", "/unstable/org.matrix.msc3882/login/get_token", { auth: uiaData })
.respond(200, response);
await httpBackend!.flush("");
expect(await prom).toStrictEqual(response);
});
it("should hit the expected API endpoint without UIA", async () => {
const response = {};
httpBackend!
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
const response = { login_token: "xyz", expires_in_ms: 5000 };
const prom = client!.requestLoginToken();
httpBackend!.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response);
await httpBackend!.flush("");
// check that expires_in has been populated for compatibility with r0
expect(await prom).toStrictEqual({ ...response, expires_in: 5 });
});
it("should hit the r1 endpoint when capability is disabled", async () => {
httpBackend!
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: false } } });
const response = { login_token: "xyz", expires_in_ms: 5000 };
const prom = client!.requestLoginToken();
httpBackend!.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response);
await httpBackend!.flush("");
// check that expires_in has been populated for compatibility with r0
expect(await prom).toStrictEqual({ ...response, expires_in: 5 });
});
it("should hit the r0 endpoint for fallback", async () => {
httpBackend!.when("GET", "/capabilities").respond(200, {});
const response = { login_token: "xyz", expires_in: 5 };
const prom = client!.requestLoginToken();
httpBackend!.when("POST", "/unstable/org.matrix.msc3882/login/token", {}).respond(200, response);
await httpBackend!.flush("");
expect(await prom).toStrictEqual(response);
// check that expires_in has been populated for compatibility with r1
expect(await prom).toStrictEqual({ ...response, expires_in_ms: 5000 });
});
});
@@ -1350,18 +1365,25 @@ describe("MatrixClient", function () {
it.each([
{
userId: "alice@localhost",
powerLevel: 100,
expectation: {
"alice@localhost": 100,
},
},
{
userId: ["alice@localhost", "bob@localhost"],
powerLevel: 100,
expectation: {
"alice@localhost": 100,
"bob@localhost": 100,
},
},
])("should modify power levels of $userId correctly", async ({ userId, expectation }) => {
{
userId: "alice@localhost",
powerLevel: undefined,
expectation: {},
},
])("should modify power levels of $userId correctly", async ({ userId, powerLevel, expectation }) => {
const event = {
getType: () => "m.room.power_levels",
getContent: () => ({
@@ -1378,7 +1400,7 @@ describe("MatrixClient", function () {
})
.respond(200, {});
const prom = client!.setPowerLevel("!room_id:server", userId, 100, event);
const prom = client!.setPowerLevel("!room_id:server", userId, powerLevel, event);
await httpBackend!.flushAllExpected();
await prom;
});
+33 -31
View File
@@ -94,16 +94,16 @@ describe("MatrixClient opts", function () {
client.stopClient();
});
it("should be able to send messages", function (done) {
it("should be able to send messages", async () => {
const eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
expect(res.event_id).toEqual(eventId);
done();
});
httpBackend.flush("/txn1", 1);
const [res] = await Promise.all([
client.sendTextMessage("!foo:bar", "a body", "txn1"),
httpBackend.flush("/txn1", 1),
]);
expect(res.event_id).toEqual(eventId);
});
it("should be able to sync / get new events", async function () {
@@ -149,7 +149,7 @@ describe("MatrixClient opts", function () {
client.stopClient();
});
it("shouldn't retry sending events", function (done) {
it("shouldn't retry sending events", async () => {
httpBackend.when("PUT", "/txn1").respond(
500,
new MatrixError({
@@ -157,19 +157,13 @@ describe("MatrixClient opts", function () {
error: "Ruh roh",
}),
);
client.sendTextMessage("!foo:bar", "a body", "txn1").then(
function (res) {
expect(false).toBe(true);
},
function (err) {
expect(err.errcode).toEqual("M_SOMETHING");
done();
},
);
httpBackend.flush("/txn1", 1);
await expect(
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
).rejects.toThrow("MatrixError: [500] Unknown message");
});
it("shouldn't queue events", function (done) {
it("shouldn't queue events", async () => {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "AAA",
});
@@ -178,30 +172,38 @@ describe("MatrixClient opts", function () {
});
let sentA = false;
let sentB = false;
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
const messageASendPromise = client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
sentA = true;
// We expect messageB to be sent before messageA to ensure as we're
// testing that there is no queueing that blocks each other
expect(sentB).toBe(true);
});
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
const messageBSendPromise = client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
sentB = true;
// We expect messageB to be sent before messageA to ensure as we're
// testing that there is no queueing that blocks each other
expect(sentA).toBe(false);
});
httpBackend.flush("/txn2", 1).then(function () {
httpBackend.flush("/txn1", 1).then(function () {
done();
});
});
// Allow messageB to succeed first
await httpBackend.flush("/txn2", 1);
// Then allow messageA to succeed
await httpBackend.flush("/txn1", 1);
// Now await the message send promises to
await messageBSendPromise;
await messageASendPromise;
});
it("should be able to send messages", function (done) {
it("should be able to send messages", async () => {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "foo",
});
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
expect(res.event_id).toEqual("foo");
done();
});
httpBackend.flush("/txn1", 1);
const [res] = await Promise.all([
client.sendTextMessage("!foo:bar", "a body", "txn1"),
httpBackend.flush("/txn1", 1),
]);
expect(res.event_id).toEqual("foo");
});
});
});
+6 -6
View File
@@ -48,13 +48,13 @@ describe("MatrixClient retrying", function () {
return httpBackend!.stop();
});
xit("should retry according to MatrixScheduler.retryFn", function () {});
it.skip("should retry according to MatrixScheduler.retryFn", function () {});
xit("should queue according to MatrixScheduler.queueFn", function () {});
it.skip("should queue according to MatrixScheduler.queueFn", function () {});
xit("should mark events as EventStatus.NOT_SENT when giving up", function () {});
it.skip("should mark events as EventStatus.NOT_SENT when giving up", function () {});
xit("should mark events as EventStatus.QUEUED when queued", function () {});
it.skip("should mark events as EventStatus.QUEUED when queued", function () {});
it("should mark events as EventStatus.CANCELLED when cancelled", function () {
// send a couple of events; the second will be queued
@@ -130,7 +130,7 @@ describe("MatrixClient retrying", function () {
});
describe("resending", function () {
xit("should be able to resend a NOT_SENT event", function () {});
xit("should be able to resend a sent event", function () {});
it.skip("should be able to resend a NOT_SENT event", function () {});
it.skip("should be able to resend a sent event", function () {});
});
});
+105 -102
View File
@@ -163,36 +163,38 @@ describe("MatrixClient room timelines", function () {
it(
"should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender",
function (done) {
client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
async () => {
const wasMessageAddedPromise = new Promise((resolve) => {
client!.on(ClientEvent.Sync, async (state) => {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client!.sendTextMessage(roomId, "I am a fish", "txn1");
// check it was added
expect(room.timeline.length).toEqual(2);
// check status
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
// check member
const member = room.timeline[1].sender;
expect(member?.userId).toEqual(userId);
expect(member?.name).toEqual(userName);
client!.sendTextMessage(roomId, "I am a fish", "txn1");
// check it was added
expect(room.timeline.length).toEqual(2);
// check status
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
// check member
const member = room.timeline[1].sender;
expect(member?.userId).toEqual(userId);
expect(member?.name).toEqual(userName);
httpBackend!.flush("/sync", 1).then(function () {
done();
await httpBackend!.flush("/sync", 1);
resolve(null);
});
});
httpBackend!.flush("/sync", 1);
await httpBackend!.flush("/sync", 1);
await wasMessageAddedPromise;
},
);
it(
"should be updated correctly when the send request finishes " +
"BEFORE the event comes down the event stream",
function (done) {
async () => {
const eventId = "$foo:bar";
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
@@ -207,28 +209,30 @@ describe("MatrixClient room timelines", function () {
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(function () {
expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend!.flush("/sync", 1).then(function () {
const wasMessageAddedPromise = new Promise((resolve) => {
client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(async () => {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
await httpBackend!.flush("/sync", 1);
expect(room.timeline[1].getId()).toEqual(eventId);
resolve(null);
});
httpBackend!.flush("/txn1", 1);
});
httpBackend!.flush("/txn1", 1);
});
httpBackend!.flush("/sync", 1);
await httpBackend!.flush("/sync", 1);
await wasMessageAddedPromise;
},
);
it(
"should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream",
function (done) {
async () => {
const eventId = "$foo:bar";
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
@@ -243,23 +247,24 @@ describe("MatrixClient room timelines", function () {
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend!.flush("/sync", 1).then(function () {
const wasMessageAddedPromise = new Promise((resolve) => {
client!.on(ClientEvent.Sync, async (state) => {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
const messageSendPromise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
await httpBackend!.flush("/sync", 1);
expect(room.timeline.length).toEqual(2);
httpBackend!.flush("/txn1", 1);
promise.then(function () {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
await messageSendPromise;
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId);
resolve(null);
});
});
httpBackend!.flush("/sync", 1);
await httpBackend!.flush("/sync", 1);
await wasMessageAddedPromise;
},
);
});
@@ -279,30 +284,29 @@ describe("MatrixClient room timelines", function () {
});
});
it("should set Room.oldState.paginationToken to null at the start" + " of the timeline.", function (done) {
client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
it("should set Room.oldState.paginationToken to null at the start of the timeline.", async () => {
const didPaginatePromise = new Promise((resolve) => {
client!.on(ClientEvent.Sync, async (state) => {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client!.scrollback(room).then(function () {
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBe(null);
// still have a sync to flush
httpBackend!.flush("/sync", 1).then(() => {
done();
});
await httpBackend!.flush("/sync", 1);
resolve(null);
});
httpBackend!.flush("/messages", 1);
});
httpBackend!.flush("/sync", 1);
await httpBackend!.flush("/sync", 1);
await didPaginatePromise;
});
it("should set the right event.sender values", function (done) {
it("should set the right event.sender values", async () => {
// We're aiming for an eventual timeline of:
//
// 'Old Alice' joined the room
@@ -353,15 +357,17 @@ describe("MatrixClient room timelines", function () {
joinMshipEvent,
];
client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
// sync response
expect(room.timeline.length).toEqual(1);
const didPaginatePromise = new Promise((resolve) => {
client!.on(ClientEvent.Sync, async (state) => {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
// sync response
expect(room.timeline.length).toEqual(1);
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
client!.scrollback(room).then(function () {
expect(room.timeline.length).toEqual(5);
const joinMsg = room.timeline[0];
expect(joinMsg.sender?.name).toEqual("Old Alice");
@@ -371,17 +377,15 @@ describe("MatrixClient room timelines", function () {
expect(newMsg.sender?.name).toEqual(userName);
// still have a sync to flush
httpBackend!.flush("/sync", 1).then(() => {
done();
});
await httpBackend!.flush("/sync", 1);
resolve(null);
});
httpBackend!.flush("/messages", 1);
});
httpBackend!.flush("/sync", 1);
await httpBackend!.flush("/sync", 1);
await didPaginatePromise;
});
it("should add it them to the right place in the timeline", function (done) {
it("should add it them to the right place in the timeline", async () => {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
@@ -396,30 +400,30 @@ describe("MatrixClient room timelines", function () {
}),
];
client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
const didPaginatePromise = new Promise((resolve) => {
client!.on(ClientEvent.Sync, async (state) => {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
client!.scrollback(room).then(function () {
expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]);
// still have a sync to flush
httpBackend!.flush("/sync", 1).then(() => {
done();
});
await httpBackend!.flush("/sync", 1);
resolve(null);
});
httpBackend!.flush("/messages", 1);
});
httpBackend!.flush("/sync", 1);
await httpBackend!.flush("/sync", 1);
await didPaginatePromise;
});
it("should use 'end' as the next pagination token", function (done) {
it("should use 'end' as the next pagination token", async () => {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
@@ -429,25 +433,24 @@ describe("MatrixClient room timelines", function () {
}),
];
client!.on(ClientEvent.Sync, function (state) {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.oldState.paginationToken).toBeTruthy();
const didPaginatePromise = new Promise((resolve) => {
client!.on(ClientEvent.Sync, async (state) => {
if (state !== "PREPARED") {
return;
}
const room = client!.getRoom(roomId)!;
expect(room.oldState.paginationToken).toBeTruthy();
client!.scrollback(room, 1).then(function () {
await Promise.all([client!.scrollback(room, 1), httpBackend!.flush("/messages", 1)]);
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend!.flush("/messages", 1).then(function () {
// still have a sync to flush
httpBackend!.flush("/sync", 1).then(() => {
done();
});
await httpBackend!.flush("/sync", 1);
resolve(null);
});
});
httpBackend!.flush("/sync", 1);
await httpBackend!.flush("/sync", 1);
await didPaginatePromise;
});
});
+16 -22
View File
@@ -81,17 +81,15 @@ describe("MatrixClient syncing", () => {
presence: {},
};
it("should /sync after /pushrules and /filter.", (done) => {
it("should /sync after /pushrules and /filter.", async () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
httpBackend!.flushAllExpected().then(() => {
done();
});
await httpBackend!.flushAllExpected();
});
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", (done) => {
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", async () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
httpBackend!
.when("GET", "/sync")
@@ -102,9 +100,7 @@ describe("MatrixClient syncing", () => {
client!.startClient();
httpBackend!.flushAllExpected().then(() => {
done();
});
await httpBackend!.flushAllExpected();
});
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
@@ -724,7 +720,7 @@ describe("MatrixClient syncing", () => {
// events that arrive in the incremental sync as if they preceeded the
// timeline events, however this breaks peeking, so it's disabled
// (see sync.js)
xit("should correctly interpret state in incremental sync.", () => {
it.skip("should correctly interpret state in incremental sync.", () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
httpBackend!.when("GET", "/sync").respond(200, nextSyncData);
@@ -741,9 +737,9 @@ describe("MatrixClient syncing", () => {
});
});
xit("should update power levels for users in a room", () => {});
it.skip("should update power levels for users in a room", () => {});
xit("should update the room topic", () => {});
it.skip("should update the room topic", () => {});
describe("onMarkerStateEvent", () => {
const normalMessageEvent = utils.mkMessage({
@@ -840,6 +836,7 @@ describe("MatrixClient syncing", () => {
roomVersion: "org.matrix.msc2716v3",
},
].forEach((testMeta) => {
// eslint-disable-next-line jest/valid-title
describe(testMeta.label, () => {
const roomCreateEvent = utils.mkEvent({
type: "m.room.create",
@@ -1592,27 +1589,24 @@ describe("MatrixClient syncing", () => {
});
describe("of a room", () => {
xit(
it.skip(
"should sync when a join event (which changes state) for the user" +
" arrives down the event stream (e.g. join from another device)",
() => {},
);
xit("should sync when the user explicitly calls joinRoom", () => {});
it.skip("should sync when the user explicitly calls joinRoom", () => {});
});
describe("syncLeftRooms", () => {
beforeEach((done) => {
beforeEach(async () => {
client!.startClient();
httpBackend!.flushAllExpected().then(() => {
// the /sync call from syncLeftRooms ends up in the request
// queue behind the call from the running client; add a response
// to flush the client's one out.
httpBackend!.when("GET", "/sync").respond(200, {});
done();
});
await httpBackend!.flushAllExpected();
// the /sync call from syncLeftRooms ends up in the request
// queue behind the call from the running client; add a response
// to flush the client's one out.
await httpBackend!.when("GET", "/sync").respond(200, {});
});
it("should create and use an appropriate filter", () => {
@@ -0,0 +1,383 @@
/*
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 "fake-indexeddb/auto";
import HttpBackend from "matrix-mock-request";
import {
Category,
ClientEvent,
EventType,
ISyncResponse,
MatrixClient,
MatrixEvent,
NotificationCountType,
RelationType,
Room,
} from "../../src";
import { TestClient } from "../TestClient";
import { ReceiptType } from "../../src/@types/read_receipts";
import { mkThread } from "../test-utils/thread";
import { SyncState } from "../../src/sync";
describe("MatrixClient syncing", () => {
const userA = "@alice:localhost";
const userB = "@bob:localhost";
const selfUserId = userA;
const selfAccessToken = "aseukfgwef";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const setupTestClient = (): [MatrixClient, HttpBackend] => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
return [client, httpBackend];
};
beforeEach(() => {
[client, httpBackend] = setupTestClient();
});
afterEach(() => {
httpBackend!.verifyNoOutstandingExpectation();
client!.stopClient();
return httpBackend!.stop();
});
it("reactions in thread set the correct timeline to unread", async () => {
const roomId = "!room:localhost";
// start the client, and wait for it to initialise
httpBackend!.when("GET", "/sync").respond(200, {
next_batch: "s_5_3",
rooms: {
[Category.Join]: {},
[Category.Leave]: {},
[Category.Invite]: {},
},
});
client!.startClient({ threadSupport: true });
await Promise.all([
httpBackend?.flushAllExpected(),
new Promise<void>((resolve) => {
client!.on(ClientEvent.Sync, (state) => state === SyncState.Syncing && resolve());
}),
]);
const room = new Room(roomId, client!, selfUserId);
jest.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)!;
room.addLiveEvents([thread.rootEvent]);
// Initialize read receipt datastructure before testing the reaction
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
thread.thread.addReceiptToStructure(
threadReply.getId()!,
ReceiptType.Read,
selfUserId,
{ thread_id: thread.thread.id, ts: 1 },
false,
);
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(threadReply.getId());
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
let lastEvent: MatrixEvent | null = null;
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
lastEvent = event as MatrixEvent;
return { event_id: reactionEventId };
});
await client!.sendEvent(roomId, EventType.Reaction, {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: threadReply.getId(),
key: "",
},
});
expect(lastEvent!.getId()).toEqual(reactionEventId);
room.handleRemoteEcho(new MatrixEvent(lastEvent!.event), lastEvent!);
// Our ideal state after this is the following:
//
// Room: [synthetic: threadroot, actual: threadroot]
// Thread: [synthetic: threadreaction, actual: threadreply]
//
// The reaction and reply are both in the thread, and their receipts should be isolated to the thread.
// The reaction has not been acknowledged in a dedicated read receipt message, so only the synthetic receipt
// should be updated.
// Ensure the synthetic receipt for the room has not been updated
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
expect(room.getEventReadUpTo(selfUserId, false)).toEqual(thread.rootEvent.getId());
// Ensure the actual receipt for the room has not been updated
expect(room.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(thread.rootEvent.getId());
expect(room.getEventReadUpTo(selfUserId, true)).toEqual(thread.rootEvent.getId());
// Ensure the synthetic receipt for the thread has been updated
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(reactionEventId);
expect(thread.thread.getEventReadUpTo(selfUserId, false)).toEqual(reactionEventId);
// Ensure the actual receipt for the thread has not been updated
expect(thread.thread.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(threadReply.getId());
expect(thread.thread.getEventReadUpTo(selfUserId, true)).toEqual(threadReply.getId());
});
describe("Stuck unread notifications integration tests", () => {
const ROOM_ID = "!room:localhost";
const syncData = getSampleStuckNotificationSyncResponse(ROOM_ID);
it("resets notifications if the last event originates from the logged in user", async () => {
httpBackend!
.when("GET", "/sync")
.check((req) => {
expect(req.queryParams!.filter).toEqual("a filter id");
})
.respond(200, syncData);
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
client!.startClient({ initialSyncLimit: 1 });
await httpBackend!.flushAllExpected();
const room = client?.getRoom(ROOM_ID);
expect(room).toBeInstanceOf(Room);
expect(room?.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
});
});
function getSampleStuckNotificationSyncResponse(roomId: string): Partial<ISyncResponse> {
return {
next_batch: "batch_token",
rooms: {
[Category.Join]: {
[roomId]: {
timeline: {
events: [
{
content: {
creator: userB,
room_version: "9",
},
origin_server_ts: 1,
sender: userB,
state_key: "",
type: "m.room.create",
event_id: "$event1",
},
{
content: {
avatar_url: "",
displayname: userB,
membership: "join",
},
origin_server_ts: 2,
sender: userB,
state_key: userB,
type: "m.room.member",
event_id: "$event2",
},
{
content: {
ban: 50,
events: {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.encryption": 100,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100,
"m.room.server_acl": 100,
"m.room.tombstone": 100,
},
events_default: 0,
historical: 100,
invite: 0,
kick: 50,
redact: 50,
state_default: 50,
users: {
[userA]: 100,
[userB]: 100,
},
users_default: 0,
},
origin_server_ts: 3,
sender: userB,
state_key: "",
type: "m.room.power_levels",
event_id: "$event3",
},
{
content: {
join_rule: "invite",
},
origin_server_ts: 4,
sender: userB,
state_key: "",
type: "m.room.join_rules",
event_id: "$event4",
},
{
content: {
history_visibility: "shared",
},
origin_server_ts: 5,
sender: userB,
state_key: "",
type: "m.room.history_visibility",
event_id: "$event5",
},
{
content: {
guest_access: "can_join",
},
origin_server_ts: 6,
sender: userB,
state_key: "",
type: "m.room.guest_access",
unsigned: {
age: 1651569,
},
event_id: "$event6",
},
{
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
origin_server_ts: 7,
sender: userB,
state_key: "",
type: "m.room.encryption",
event_id: "$event7",
},
{
content: {
avatar_url: "",
displayname: userA,
is_direct: true,
membership: "invite",
},
origin_server_ts: 8,
sender: userB,
state_key: userA,
type: "m.room.member",
event_id: "$event8",
},
{
content: {
msgtype: "m.text",
body: "hello",
},
origin_server_ts: 9,
sender: userB,
type: "m.room.message",
event_id: "$event9",
},
{
content: {
avatar_url: "",
displayname: userA,
membership: "join",
},
origin_server_ts: 10,
sender: userA,
state_key: userA,
type: "m.room.member",
event_id: "$event10",
},
{
content: {
msgtype: "m.text",
body: "world",
},
origin_server_ts: 11,
sender: userA,
type: "m.room.message",
event_id: "$event11",
},
],
prev_batch: "123",
limited: false,
},
state: {
events: [],
},
account_data: {
events: [
{
type: "m.fully_read",
content: {
event_id: "$dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU",
},
},
],
},
ephemeral: {
events: [
{
type: "m.receipt",
content: {
$event9: {
"m.read": {
[userA]: {
ts: 100,
},
},
"m.read.private": {
[userA]: {
ts: 100,
},
},
},
dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU: {
"m.read": {
[userB]: {
ts: 666,
},
},
},
},
},
],
},
unread_notifications: {
notification_count: 1,
highlight_count: 0,
},
summary: {
"m.joined_member_count": 2,
"m.invited_member_count": 0,
"m.heroes": [userB],
},
},
},
[Category.Leave]: {},
[Category.Invite]: {},
},
};
}
});
+4 -4
View File
@@ -29,13 +29,13 @@ limitations under the License.
import "../olm-loader";
import type { Session } from "@matrix-org/olm";
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
import { logger } from "../../src/logger";
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration";
let aliTestClient: TestClient;
const roomId = "!room:localhost";
@@ -472,7 +472,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
@@ -483,7 +483,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
@@ -545,7 +545,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
+17 -30
View File
@@ -153,11 +153,11 @@ describe("SlidingSyncSdk", () => {
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
expect(mockSlidingSync!.start).toBeCalled();
expect(mockSlidingSync!.start).toHaveBeenCalled();
});
it("can stop()", async () => {
sdk!.stop();
expect(mockSlidingSync!.stop).toBeCalled();
expect(mockSlidingSync!.stop).toHaveBeenCalled();
});
});
@@ -584,7 +584,7 @@ describe("SlidingSyncSdk", () => {
});
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
expect(mockSlidingSync!.stop).not.toBeCalled();
expect(mockSlidingSync!.stop).not.toHaveBeenCalled();
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle,
SlidingSyncState.RequestFinished,
@@ -595,7 +595,7 @@ describe("SlidingSyncSdk", () => {
}),
);
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
expect(mockSlidingSync!.stop).toBeCalled();
expect(mockSlidingSync!.stop).toHaveBeenCalled();
});
});
@@ -662,41 +662,30 @@ describe("SlidingSyncSdk", () => {
});
it("can update device lists", () => {
client!.crypto!.processDeviceLists = jest.fn();
ext.onResponse({
device_lists: {
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
},
});
// TODO: more assertions?
expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
});
});
it("can update OTK counts", () => {
client!.crypto!.updateOneTimeKeyCount = jest.fn();
it("can update OTK counts and unused fallback keys", () => {
client!.crypto!.processKeyCounts = jest.fn();
ext.onResponse({
device_one_time_keys_count: {
signed_curve25519: 42,
},
});
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
ext.onResponse({
device_one_time_keys_count: {
not_signed_curve25519: 42,
// missing field -> default to 0
},
});
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
});
it("can update fallback keys", () => {
ext.onResponse({
device_unused_fallback_key_types: ["signed_curve25519"],
});
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
ext.onResponse({
device_unused_fallback_key_types: ["not_signed_curve25519"],
});
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
"signed_curve25519",
]);
});
});
@@ -891,11 +880,9 @@ describe("SlidingSyncSdk", () => {
const evType = ev.getType();
expect(seen[evType]).toBeFalsy();
seen[evType] = true;
if (evType === "m.key.verification.start" || evType === "m.key.verification.request") {
expect(ev.isCancelled()).toEqual(true);
} else {
expect(ev.isCancelled()).toEqual(false);
}
expect(ev.isCancelled()).toEqual(
evType === "m.key.verification.start" || evType === "m.key.verification.request",
);
});
ext.onResponse({
next_batch: "45678",
+1 -1
View File
@@ -16,7 +16,7 @@ limitations under the License.
import DOMException from "domexception";
global.DOMException = DOMException;
global.DOMException = DOMException as typeof global.DOMException;
jest.mock("../src/http-api/utils", () => ({
...jest.requireActual("../src/http-api/utils"),
+159
View File
@@ -0,0 +1,159 @@
/*
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 debugFunc from "debug";
import { Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
/** Interface implemented by classes that intercept `/keys/upload` requests from test clients to catch the uploaded keys
*
* Common interface implemented by {@link TestClient} and {@link E2EKeyReceiver}
*/
export interface IE2EKeyReceiver {
/**
* get the uploaded ed25519 device key
*
* @returns base64 device key
*/
getSigningKey(): string;
/**
* get the uploaded curve25519 device key
*
* @returns base64 device key
*/
getDeviceKey(): string;
/**
* Wait for one-time-keys to be uploaded, then return them.
*
* @returns Promise for the one-time keys
*/
awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>>;
}
/** E2EKeyReceiver: An object which intercepts `/keys/uploads` fetches via fetch-mock.
*
* It stashes the uploaded keys for use elsewhere in the tests.
*/
export class E2EKeyReceiver implements IE2EKeyReceiver {
private readonly debug: Debugger;
private deviceKeys: IDeviceKeys | null = null;
private oneTimeKeys: Record<string, IOneTimeKey> = {};
private readonly oneTimeKeysPromise: Promise<void>;
/**
* Construct a new E2EKeyReceiver.
*
* It will immediately register an intercept of `/keys/uploads` requests for the given homeserverUrl.
* Only /upload requests made to this server will be intercepted: this allows a single test to use more than one
* client and have the keys collected separately.
*
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
this.debug = debugFunc(`e2e-key-receiver:[${homeserverUrl}]`);
// set up a listener for /keys/upload.
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
const listener = (url: string, options: RequestInit) =>
this.onKeyUploadRequest(resolveOneTimeKeys, options);
// catch both r0 and v3 variants
fetchMock.post(new URL("/_matrix/client/r0/keys/upload", homeserverUrl).toString(), listener);
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
});
}
private async onKeyUploadRequest(onOnTimeKeysUploaded: () => void, options: RequestInit): Promise<object> {
const content = JSON.parse(options.body as string);
// device keys may only be uploaded once
if (content.device_keys && Object.keys(content.device_keys).length > 0) {
if (this.deviceKeys) {
throw new Error("Application attempted to upload E2E device keys multiple times");
}
this.debug(`received device keys`);
this.deviceKeys = content.device_keys;
}
if (content.one_time_keys && Object.keys(content.one_time_keys).length > 0) {
// this is a one-time-key upload
// if we already have a batch of one-time keys, then slow-roll the response,
// otherwise the client ends up tight-looping one-time-key-uploads and filling the logs with junk.
if (Object.keys(this.oneTimeKeys).length > 0) {
this.debug(`received second batch of one-time keys: blocking response`);
await new Promise(() => {});
}
this.debug(`received ${Object.keys(content.one_time_keys).length} one-time keys`);
Object.assign(this.oneTimeKeys, content.one_time_keys);
onOnTimeKeysUploaded();
}
return {
one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
},
};
}
/** Get the uploaded Ed25519 key
*
* If device keys have not yet been uploaded, throws an error
*/
public getSigningKey(): string {
if (!this.deviceKeys) {
throw new Error("Device keys not yet uploaded");
}
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("ed25519:"));
if (keyIds.length != 1) {
throw new Error(`Expected exactly 1 ed25519 key uploaded, got ${keyIds}`);
}
return this.deviceKeys.keys[keyIds[0]];
}
/** Get the uploaded Curve25519 key
*
* If device keys have not yet been uploaded, throws an error
*/
public getDeviceKey(): string {
if (!this.deviceKeys) {
throw new Error("Device keys not yet uploaded");
}
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("curve25519:"));
if (keyIds.length != 1) {
throw new Error(`Expected exactly 1 curve25519 key uploaded, got ${keyIds}`);
}
return this.deviceKeys.keys[keyIds[0]];
}
/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns Promise for the one-time keys
*/
public async awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
await this.oneTimeKeysPromise;
return this.oneTimeKeys;
}
}
+131
View File
@@ -0,0 +1,131 @@
/*
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 debugFunc from "debug";
import { Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import { MockResponse } from "fetch-mock";
/** Interface implemented by classes that intercept `/sync` requests from test clients
*
* Common interface implemented by {@link TestClient} and {@link SyncResponder}
*/
export interface ISyncResponder {
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
*
* @param response - response to /sync request
*/
sendOrQueueSyncResponse(response: object): void;
}
enum SyncResponderState {
IDLE,
WAITING_FOR_REQUEST,
WAITING_FOR_RESPONSE,
}
/** SyncResponder: An object which intercepts `/sync` fetches via fetch-mock.
*
* Two modes are possible:
* * A response can be queued up; the next call to `/sync` will return it.
* * If a call to `/sync` arrives before a response is queued, it will block until a call to {@link #sendOrQueueSyncResponse}.
*/
export class SyncResponder implements ISyncResponder {
private readonly debug: Debugger;
private state: SyncResponderState = SyncResponderState.IDLE;
/*
* properties that are only valid in WAITING_FOR_REQUEST
*/
/** the response to be sent when the request is made */
private pendingResponse: object | null = null;
/*
* properties that are only valid in WAITING_FOR_RESPONSE
*/
/** a callback to be called with a response once one is registered.
*
* It will release the /sync request and update the state.
*/
private onResponseReceived: ((response: object) => void) | null = null;
/**
* Construct a new SyncResponder.
*
* It will immediately register an intercept of `/sync` requests for the given homeserverUrl.
* Only /sync requests made to this server will be intercepted: this allows a single test to use more than one
* client and have overlapping /sync requests.
*
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
fetchMock.get("begin:" + new URL("/_matrix/client/r0/sync?", homeserverUrl).toString(), (_url, _options) =>
this.onSyncRequest(),
);
}
private async onSyncRequest(): Promise<MockResponse> {
switch (this.state) {
case SyncResponderState.IDLE: {
this.debug("Got /sync request: waiting for response to be ready");
const res = await new Promise<object>((resolve) => {
this.onResponseReceived = resolve;
this.state = SyncResponderState.WAITING_FOR_RESPONSE;
});
this.debug("Responding to /sync");
this.state = SyncResponderState.IDLE;
this.onResponseReceived = null;
return res;
}
case SyncResponderState.WAITING_FOR_REQUEST: {
this.debug("Got /sync request: responding immediately with queued response");
const res = this.pendingResponse!;
this.state = SyncResponderState.IDLE;
this.pendingResponse = null;
return res;
}
default:
// we must already be in WAITING_FOR_RESPONSE, ie we already have a /sync request in progress
throw new Error(`Got unexpected /sync request in state ${this.state}`);
}
}
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
*
* @param response - response to /sync request
*/
public sendOrQueueSyncResponse(response: object): void {
switch (this.state) {
case SyncResponderState.IDLE:
this.pendingResponse = response;
this.state = SyncResponderState.WAITING_FOR_REQUEST;
break;
case SyncResponderState.WAITING_FOR_RESPONSE:
this.onResponseReceived!(response);
break;
default:
// we already have a response queued
throw new Error(`Cannot queue more than one /sync response`);
}
}
}
+4 -4
View File
@@ -375,17 +375,17 @@ export async function awaitDecryption(
// already
if (event.getClearContent() !== null) {
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
logger.log(`${Date.now()} event ${event.getId()} got decryption error; waiting`);
logger.log(`${Date.now()}: event ${event.getId()} got decryption error; waiting`);
} else {
return event;
}
} else {
logger.log(`${Date.now()} event ${event.getId()} is not yet decrypted; waiting`);
logger.log(`${Date.now()}: event ${event.getId()} is not yet decrypted; waiting`);
}
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
resolve(ev);
});
});
+149 -6
View File
@@ -30,6 +30,7 @@ import {
RoomState,
RoomStateEvent,
RoomStateEventHandlerMap,
SendToDeviceContentMap,
} from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
@@ -122,6 +123,7 @@ export class MockRTCPeerConnection {
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
public iceConnectionStateChangeListener?: () => void;
public onTrackListener?: (e: RTCTrackEvent) => void;
public onDataChannelListener?: (ev: RTCDataChannelEvent) => void;
public needsNegotiation = false;
public readyToNegotiate: Promise<void>;
private onReadyToNegotiate?: () => void;
@@ -167,6 +169,8 @@ export class MockRTCPeerConnection {
this.iceConnectionStateChangeListener = listener;
} else if (type == "track") {
this.onTrackListener = listener;
} else if (type == "datachannel") {
this.onDataChannelListener = listener;
}
}
public createDataChannel(label: string, opts: RTCDataChannelInit) {
@@ -231,6 +235,10 @@ export class MockRTCPeerConnection {
this.negotiationNeededListener();
}
}
public triggerIncomingDataChannel(): void {
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
}
}
export class MockRTCRtpSender {
@@ -443,13 +451,13 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
>();
public sendToDevice = jest.fn<
Promise<{}>,
[
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
txnId?: string,
]
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
>();
public isInitialSyncComplete(): boolean {
return false;
}
public getMediaHandler(): MediaHandler {
return this.mediaHandler.typed();
}
@@ -498,18 +506,22 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
public state = CallState.Ringing;
public opponentUserId = FAKE_USER_ID_1;
public opponentDeviceId = FAKE_DEVICE_ID_1;
public opponentSessionId = FAKE_SESSION_ID_1;
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),
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 sendMetadataUpdate = jest.fn<void, []>();
@@ -521,6 +533,14 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
return this.opponentDeviceId;
}
public getOpponentSessionId(): string | undefined {
return this.opponentSessionId;
}
public getLocalFeeds(): CallFeed[] {
return [this.localUsermediaFeed];
}
public typed(): MatrixCall {
return this as unknown as MatrixCall;
}
@@ -581,6 +601,7 @@ export function makeMockGroupCallStateEvent(
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
},
redacted?: boolean,
): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
@@ -588,6 +609,7 @@ export function makeMockGroupCallStateEvent(
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue(content),
getStateKey: jest.fn().mockReturnValue(groupCallId),
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
} as unknown as MatrixEvent;
}
@@ -600,3 +622,124 @@ export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: s
getStateKey: jest.fn().mockReturnValue(groupCallId),
} as unknown as MatrixEvent;
}
export const REMOTE_SFU_DESCRIPTION =
"v=0\n" +
"o=- 3242942315779688438 1678878001 IN IP4 0.0.0.0\n" +
"s=-\n" +
"t=0 0\n" +
"a=fingerprint:sha-256 EA:30:B2:7F:49:B5:46:D6:40:72:BF:79:95:C1:65:08:6E:35:09:FB:90:89:DA:EF:6B:82:D1:38:8C:25:39:B2\n" +
"a=group:BUNDLE 0 1 2\n" +
"m=audio 9 UDP/TLS/RTP/SAVPF 111 9 0 8\n" +
"c=IN IP4 0.0.0.0\n" +
"a=setup:actpass\n" +
"a=mid:0\n" +
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
"a=rtcp-mux\n" +
"a=rtcp-rsize\n" +
"a=rtpmap:111 opus/48000/2\n" +
"a=fmtp:111 minptime=10;usedtx=1;useinbandfec=1\n" +
"a=rtcp-fb:111 transport-cc \n" +
"a=rtpmap:9 G722/8000\n" +
"a=rtpmap:0 PCMU/8000\n" +
"a=rtpmap:8 PCMA/8000\n" +
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
"a=ssrc:2963372119 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
"a=ssrc:2963372119 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
"a=ssrc:2963372119 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
"a=ssrc:2963372119 label:4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
"a=sendrecv\n" +
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 41385 typ host\n" +
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 41385 typ host\n" +
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 40026 typ host\n" +
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 40026 typ host\n" +
"a=end-of-candidates\n" +
"m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 103 104 106 108 109 98 99 112 116\n" +
"c=IN IP4 0.0.0.0\n" +
"a=setup:actpass\n" +
"a=mid:1\n" +
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
"a=rtcp-mux\n" +
"a=rtcp-rsize\n" +
"a=rtpmap:96 VP8/90000\n" +
"a=rtcp-fb:96 goog-remb \n" +
"a=rtcp-fb:96 transport-cc \n" +
"a=rtcp-fb:96 ccm fir\n" +
"a=rtcp-fb:96 nack \n" +
"a=rtcp-fb:96 nack pli\n" +
"a=rtpmap:97 rtx/90000\n" +
"a=fmtp:97 apt=96\n" +
"a=rtpmap:102 H264/90000\n" +
"a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\n" +
"a=rtcp-fb:102 goog-remb \n" +
"a=rtcp-fb:102 transport-cc \n" +
"a=rtcp-fb:102 ccm fir\n" +
"a=rtcp-fb:102 nack \n" +
"a=rtcp-fb:102 nack pli\n" +
"a=rtpmap:103 rtx/90000\n" +
"a=fmtp:103 apt=102\n" +
"a=rtpmap:104 H264/90000\n" +
"a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\n" +
"a=rtcp-fb:104 goog-remb \n" +
"a=rtcp-fb:104 transport-cc \n" +
"a=rtcp-fb:104 ccm fir\n" +
"a=rtcp-fb:104 nack \n" +
"a=rtcp-fb:104 nack pli\n" +
"a=rtpmap:106 H264/90000\n" +
"a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\n" +
"a=rtcp-fb:106 goog-remb \n" +
"a=rtcp-fb:106 transport-cc \n" +
"a=rtcp-fb:106 ccm fir\n" +
"a=rtcp-fb:106 nack \n" +
"a=rtcp-fb:106 nack pli\n" +
"a=rtpmap:108 H264/90000\n" +
"a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\n" +
"a=rtcp-fb:108 goog-remb \n" +
"a=rtcp-fb:108 transport-cc \n" +
"a=rtcp-fb:108 ccm fir\n" +
"a=rtcp-fb:108 nack \n" +
"a=rtcp-fb:108 nack pli\n" +
"a=rtpmap:109 rtx/90000\n" +
"a=fmtp:109 apt=108\n" +
"a=rtpmap:98 VP9/90000\n" +
"a=fmtp:98 profile-id=0\n" +
"a=rtcp-fb:98 goog-remb \n" +
"a=rtcp-fb:98 transport-cc \n" +
"a=rtcp-fb:98 ccm fir\n" +
"a=rtcp-fb:98 nack \n" +
"a=rtcp-fb:98 nack pli\n" +
"a=rtpmap:99 rtx/90000\n" +
"a=fmtp:99 apt=98\n" +
"a=rtpmap:112 H264/90000\n" +
"a=fmtp:112 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\n" +
"a=rtcp-fb:112 goog-remb \n" +
"a=rtcp-fb:112 transport-cc \n" +
"a=rtcp-fb:112 ccm fir\n" +
"a=rtcp-fb:112 nack \n" +
"a=rtcp-fb:112 nack pli\n" +
"a=rtpmap:116 ulpfec/90000\n" +
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\n" +
"a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\n" +
"a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\n" +
"a=rid:f recv\n" +
"a=rid:h recv\n" +
"a=rid:q recv\n" +
"a=simulcast:recv f;h;q\n" +
"a=ssrc:1212931603 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
"a=ssrc:1212931603 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
"a=ssrc:1212931603 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
"a=ssrc:1212931603 label:12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
"a=sendrecv\n" +
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n" +
"c=IN IP4 0.0.0.0\n" +
"a=setup:actpass\n" +
"a=mid:2\n" +
"a=sendrecv\n" +
"a=sctp-port:5000\n" +
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs";
+2 -14
View File
@@ -46,13 +46,7 @@ describe("NamespacedValue", () => {
});
it("should not permit falsey values for both parts", () => {
try {
new UnstableValue(null!, null!);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
}
expect(() => new UnstableValue(null!, null!)).toThrow("One of stable or unstable values must be supplied");
});
});
@@ -72,12 +66,6 @@ describe("UnstableValue", () => {
});
it("should not permit falsey unstable values", () => {
try {
new UnstableValue("stable", null!);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect((<Error>e).message).toBe("Unstable value must be supplied");
}
expect(() => new UnstableValue("stable", null!)).toThrow("Unstable value must be supplied");
});
});
+16 -6
View File
@@ -5,6 +5,7 @@ import { getMockClientWithEventEmitter } from "../test-utils/client";
import { StubStore } from "../../src/store/stub";
import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { SyncState } from "../../src/sync";
import { defer } from "../../src/utils";
describe("onResumedSync", () => {
let batch: IndexedToDeviceBatch | null;
@@ -58,7 +59,9 @@ describe("onResumedSync", () => {
queue = new ToDeviceMessageQueue(mockClient);
});
it("resends queue after connectivity restored", (done) => {
it("resends queue after connectivity restored", async () => {
const deferred = defer();
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
@@ -70,26 +73,32 @@ describe("onResumedSync", () => {
onSendToDeviceSuccess = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3);
expect(store.removeToDeviceBatch).toHaveBeenCalled();
done();
deferred.resolve();
};
queue.start();
return deferred.promise;
});
it("does not resend queue if client sync still catching up", (done) => {
it("does not resend queue if client sync still catching up", async () => {
const deferred = defer();
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
resumeSync(SyncState.Catchup, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
done();
deferred.resolve();
};
queue.start();
return deferred.promise;
});
it("does not resend queue if connectivity restored after queue stopped", (done) => {
it("does not resend queue if connectivity restored after queue stopped", async () => {
const deferred = defer();
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
@@ -98,9 +107,10 @@ describe("onResumedSync", () => {
resumeSync(SyncState.Syncing, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
done();
deferred.resolve();
};
queue.start();
return deferred.promise;
});
});
+41 -29
View File
@@ -304,20 +304,24 @@ describe("Crypto", function () {
describe("Key requests", function () {
let aliceClient: MatrixClient;
let secondAliceClient: MatrixClient;
let bobClient: MatrixClient;
let claraClient: MatrixClient;
beforeEach(async function () {
aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client;
bobClient = new TestClient("@bob:example.com", "bobdevice").client;
claraClient = new TestClient("@clara:example.com", "claradevice").client;
await aliceClient.initCrypto();
await secondAliceClient.initCrypto();
await bobClient.initCrypto();
await claraClient.initCrypto();
});
afterEach(async function () {
aliceClient.stopClient();
secondAliceClient.stopClient();
bobClient.stopClient();
claraClient.stopClient();
});
@@ -401,7 +405,7 @@ describe("Crypto", function () {
// the first message can't be decrypted yet, but the second one
// can
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
await bobDecryptor.onRoomKeyEvent(ksEvent);
await decryptEventsPromise;
@@ -550,7 +554,7 @@ describe("Crypto", function () {
aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests();
jest.runAllTimers();
await Promise.resolve();
expect(aliceSendToDevice).toBeCalledTimes(1);
expect(aliceSendToDevice).toHaveBeenCalledTimes(1);
const txnId = aliceSendToDevice.mock.calls[0][2];
// give the room key request manager time to update the state
@@ -564,21 +568,21 @@ describe("Crypto", function () {
// cancelAndResend will call sendToDevice twice:
// the first call to sendToDevice will be the cancellation
// the second call to sendToDevice will be the key request
expect(aliceSendToDevice).toBeCalledTimes(3);
expect(aliceSendToDevice).toHaveBeenCalledTimes(3);
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
});
it("should accept forwarded keys which it requested", async function () {
it("should accept forwarded keys it requested from one of its own user's other devices", async function () {
const encryptionCfg = {
algorithm: "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
const bobRoom = new Room(roomId, secondAliceClient, "@alice:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
secondAliceClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
await secondAliceClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
@@ -614,7 +618,7 @@ describe("Crypto", function () {
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto!.decryptEvent(event);
await secondAliceClient.crypto!.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
@@ -623,10 +627,11 @@ describe("Crypto", function () {
);
const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
secondAliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
secondAliceClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const cryptoStore = bobClient.crypto!.cryptoStore;
const cryptoStore = secondAliceClient.crypto!.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
@@ -642,7 +647,7 @@ describe("Crypto", function () {
state: RoomKeyRequestState.Sent,
});
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
const bobDecryptor = secondAliceClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
const decryptEventsPromise = Promise.all(
events.map((ev) => {
@@ -651,7 +656,7 @@ describe("Crypto", function () {
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
const key = await secondAliceClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
@@ -755,7 +760,7 @@ describe("Crypto", function () {
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
});
it("should accept forwarded keys from one of its own user's other devices", async function () {
it("should not accept requested forwarded keys from other users", async function () {
const encryptionCfg = {
algorithm: "m.megolm.v1.aes-sha2",
};
@@ -809,31 +814,39 @@ describe("Crypto", function () {
}),
);
const device = new DeviceInfo(claraClient.deviceId!);
const cryptoStore = bobClient.crypto!.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
session_id: sessionId,
};
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
expect(outgoingReq).toBeDefined();
await cryptoStore.updateOutgoingRoomKeyRequest(outgoingReq!.requestId, RoomKeyRequestState.Unsent, {
state: RoomKeyRequestState.Sent,
});
const device = new DeviceInfo(aliceClient.deviceId!);
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com";
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
const decryptEventsPromise = Promise.all(
events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}),
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = bobClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!);
ksEvent.event.sender = aliceClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, aliceClient.getUserId()!);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).not.toBeNull();
await decryptEventsPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(key).toBeNull();
});
it("should not accept unexpected forwarded keys for a room it's in", async function () {
@@ -998,7 +1011,6 @@ describe("Crypto", function () {
jest.setTimeout(10000);
const client = new TestClient("@a:example.com", "dev").client;
await client.initCrypto();
client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null);
client.crypto!.isCrossSigningReady = async () => false;
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
@@ -1026,7 +1038,7 @@ describe("Crypto", function () {
beforeEach(async () => {
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
ensureOlmSessionsForDevices.mockResolvedValue({});
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
result.plaintext = { type: 0, body: JSON.stringify(payload) };
+6 -5
View File
@@ -102,9 +102,10 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
expect(getCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
if (shouldCache) {
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
// eslint-disable-next-line jest/no-conditional-expect
expect(getCrossSigningKeyCache).toHaveBeenLastCalledWith(type, expect.any(String));
}
},
);
@@ -115,10 +116,10 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
expect(storeCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
if (shouldCache) {
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
// eslint-disable-next-line jest/no-conditional-expect
expect(storeCrossSigningKeyCache).toHaveBeenLastCalledWith(type, testKey);
}
});
+5 -1
View File
@@ -148,6 +148,10 @@ describe("DeviceList", function () {
dl.invalidateUserDeviceList("@test1:sw1v.org");
dl.refreshOutdatedDeviceLists();
// TODO: Fix this test so we actually await the call and assertions and remove
// the eslint disable, https://github.com/matrix-org/matrix-js-sdk/issues/2977
//
// eslint-disable-next-line jest/valid-expect-in-promise
dl.saveIfDirty()
.then(() => {
// the first request completes
@@ -196,7 +200,7 @@ describe("DeviceList", function () {
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toBeCalledTimes(2);
expect(downloadSpy).toHaveBeenCalledTimes(2);
expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
+59 -45
View File
@@ -34,6 +34,7 @@ import { ClientEvent, MatrixClient, RoomMember } from "../../../../src";
import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning";
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
import { recursiveMapToObject } from "../../../../src/utils";
import { sleep } from "../../../../src/utils";
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
@@ -183,14 +184,22 @@ describe("MegolmDecryption", function () {
const deviceInfo = {} as DeviceInfo;
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
"@alice:foo": {
alidevice: {
sessionId: "alisession",
device: new DeviceInfo("alidevice"),
},
},
});
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue(
new Map([
[
"@alice:foo",
new Map([
[
"alidevice",
{
sessionId: "alisession",
device: new DeviceInfo("alidevice"),
},
],
]),
],
]),
);
const awaitEncryptForDevice = new Promise<void>((res, rej) => {
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
@@ -211,7 +220,7 @@ describe("MegolmDecryption", function () {
.then(() => {
// check that it called encryptMessageForDevice with
// appropriate args.
expect(mockOlmLib.encryptMessageForDevice).toBeCalledTimes(1);
expect(mockOlmLib.encryptMessageForDevice).toHaveBeenCalledTimes(1);
const call = mockOlmLib.encryptMessageForDevice.mock.calls[0];
const payload = call[6];
@@ -357,11 +366,7 @@ describe("MegolmDecryption", function () {
} as unknown as DeviceInfo;
mockCrypto.downloadKeys.mockReturnValue(
Promise.resolve({
"@alice:home.server": {
aliceDevice: aliceDeviceInfo,
},
}),
Promise.resolve(new Map([["@alice:home.server", new Map([["aliceDevice", aliceDeviceInfo]])]])),
);
mockCrypto.checkDeviceTrust.mockReturnValue({
@@ -523,23 +528,32 @@ describe("MegolmDecryption", function () {
let megolm: MegolmEncryptionClass;
let room: jest.Mocked<Room>;
const deviceMap: DeviceInfoMap = {
"user-a": {
"device-a": new DeviceInfo("device-a"),
"device-b": new DeviceInfo("device-b"),
"device-c": new DeviceInfo("device-c"),
},
"user-b": {
"device-d": new DeviceInfo("device-d"),
"device-e": new DeviceInfo("device-e"),
"device-f": new DeviceInfo("device-f"),
},
"user-c": {
"device-g": new DeviceInfo("device-g"),
"device-h": new DeviceInfo("device-h"),
"device-i": new DeviceInfo("device-i"),
},
};
const deviceMap: DeviceInfoMap = new Map([
[
"user-a",
new Map([
["device-a", new DeviceInfo("device-a")],
["device-b", new DeviceInfo("device-b")],
["device-c", new DeviceInfo("device-c")],
]),
],
[
"user-b",
new Map([
["device-d", new DeviceInfo("device-d")],
["device-e", new DeviceInfo("device-e")],
["device-f", new DeviceInfo("device-f")],
]),
],
[
"user-c",
new Map([
["device-g", new DeviceInfo("device-g")],
["device-h", new DeviceInfo("device-h")],
["device-i", new DeviceInfo("device-i")],
]),
],
]);
beforeEach(() => {
room = testUtils.mock(Room, "Room") as jest.Mocked<Room>;
@@ -572,8 +586,8 @@ describe("MegolmDecryption", function () {
//@ts-ignore private member access, gross
await megolm.encryptionPreparation?.promise;
for (const userId in deviceMap) {
for (const deviceId in deviceMap[userId]) {
for (const [userId, devices] of deviceMap) {
for (const deviceId of devices.keys()) {
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledWith(userId, deviceId);
}
}
@@ -658,20 +672,20 @@ describe("MegolmDecryption", function () {
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice1["org.matrix.msgid"];
delete contentMap["@bob:example.com"].bobdevice2.session_id;
delete contentMap["@bob:example.com"].bobdevice2["org.matrix.msgid"];
expect(contentMap).toStrictEqual({
"@bob:example.com": {
bobdevice1: {
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["session_id"];
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["org.matrix.msgid"];
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["session_id"];
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["org.matrix.msgid"];
expect(recursiveMapToObject(contentMap)).toStrictEqual({
["@bob:example.com"]: {
["bobdevice1"]: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: "m.unverified",
reason: "The sender has disabled encrypting to unverified devices.",
sender_key: aliceDevice.deviceCurve25519Key,
},
bobdevice2: {
["bobdevice2"]: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: "m.blacklisted",
@@ -839,10 +853,10 @@ describe("MegolmDecryption", function () {
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"]["bobdevice"]["org.matrix.msgid"];
expect(contentMap).toStrictEqual({
"@bob:example.com": {
bobdevice: {
delete contentMap.get("@bob:example.com")?.get("bobdevice")?.["org.matrix.msgid"];
expect(recursiveMapToObject(contentMap)).toStrictEqual({
["@bob:example.com"]: {
["bobdevice"]: {
algorithm: "m.megolm.v1.aes-sha2",
code: "m.no_olm",
reason: "Unable to establish a secure channel.",
+15 -16
View File
@@ -146,18 +146,21 @@ describe("OlmDevice", function () {
});
},
} as unknown as MockedObject<MatrixClient>;
const devicesByUser = {
"@bob:example.com": [
DeviceInfo.fromStorage(
{
keys: {
"curve25519:ABCDEFG": "akey",
const devicesByUser = new Map([
[
"@bob:example.com",
[
DeviceInfo.fromStorage(
{
keys: {
"curve25519:ABCDEFG": "akey",
},
},
},
"ABCDEFG",
),
"ABCDEFG",
),
],
],
};
]);
// start two tasks that try to ensure that there's an olm session
const promises = Promise.all([
@@ -218,12 +221,8 @@ describe("OlmDevice", function () {
// There's no required ordering of devices per user, so here we
// create two different orderings so that each task reserves a
// device the other task needs before continuing.
const devicesByUserAB = {
"@bob:example.com": [deviceBobA, deviceBobB],
};
const devicesByUserBA = {
"@bob:example.com": [deviceBobB, deviceBobA],
};
const devicesByUserAB = new Map([["@bob:example.com", [deviceBobA, deviceBobB]]]);
const devicesByUserBA = new Map([["@bob:example.com", [deviceBobB, deviceBobA]]]);
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserAB));
+2
View File
@@ -456,6 +456,7 @@ describe("MegolmBackup", function () {
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
/* eslint-disable jest/no-conditional-expect */
if (numCalls === 1) {
expect(method).toBe("POST");
expect(path).toBe("/room_keys/version");
@@ -482,6 +483,7 @@ describe("MegolmBackup", function () {
reject(new Error("authedRequest called too many times"));
return Promise.resolve({});
}
/* eslint-enable jest/no-conditional-expect */
};
}),
client.createKeyBackupVersion({
+1 -1
View File
@@ -1148,6 +1148,6 @@ describe("userHasCrossSigningKeys", function () {
it("throws an error if crypto is disabled", () => {
aliceClient["cryptoBackend"] = undefined;
expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled");
expect(() => aliceClient.userHasCrossSigningKeys()).toThrow("encryption disabled");
});
});
+6 -5
View File
@@ -17,7 +17,6 @@ limitations under the License.
import "../../olm-loader";
import * as olmlib from "../../../src/crypto/olmlib";
import { IObject } from "../../../src/crypto/olmlib";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from "../../TestClient";
import { makeTestClients } from "./verification/util";
@@ -25,10 +24,10 @@ import { encryptAES } from "../../../src/crypto/aes";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
import { ISecretStorageKeyInfo } from "../../../src/crypto/api";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { ISignatures } from "../../../src/@types/signed";
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
async function makeTestClient(
userInfo: { userId: string; deviceId: string },
@@ -45,7 +44,7 @@ async function makeTestClient(
await client.initCrypto();
// No need to download keys for these tests
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue({});
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map());
return client;
}
@@ -274,7 +273,7 @@ describe("Secrets", function () {
Object.values(otks)[0],
);
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const request = await secretStorage.request("foo", ["VAX"]);
@@ -541,7 +540,9 @@ describe("Secrets", function () {
await alice.bootstrapSecretStorage({});
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()).toEqual({ key: "key_id" });
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
const keyInfo = alice
.getAccountData("m.secret_storage.key.key_id")!
.getContent<SecretStorageKeyDescription>();
expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({
algorithm: "m.pbkdf2",
+22 -11
View File
@@ -121,12 +121,12 @@ describe("SAS verification", function () {
alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES);
alice.client.downloadKeys = () => {
return Promise.resolve({});
return Promise.resolve(new Map());
};
bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES);
bob.client.downloadKeys = () => {
return Promise.resolve({});
return Promise.resolve(new Map());
};
aliceSasEvent = null;
@@ -176,6 +176,7 @@ describe("SAS verification", function () {
}
});
});
afterEach(async () => {
await Promise.all([alice.stop(), bob.stop()]);
@@ -186,10 +187,14 @@ describe("SAS verification", function () {
let macMethod;
let keyAgreement;
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = function (type, map) {
bob.client.sendToDevice = async (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!].key_agreement_protocol;
macMethod = map
.get(alice.client.getUserId()!)
?.get(alice.client.deviceId!)?.message_authentication_code;
keyAgreement = map
.get(alice.client.getUserId()!)
?.get(alice.client.deviceId!)?.key_agreement_protocol;
}
return origSendToDevice(type, map);
};
@@ -237,7 +242,7 @@ describe("SAS verification", function () {
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = [
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
"hkdf-hmac-sha256",
];
}
@@ -246,7 +251,9 @@ describe("SAS verification", function () {
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
macMethod = map
.get(alice.client.getUserId()!)!
.get(alice.client.deviceId!)!.message_authentication_code;
}
return bobOrigSendToDevice(type, map);
};
@@ -291,14 +298,18 @@ describe("SAS verification", function () {
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = ["hmac-sha256"];
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
"hmac-sha256",
];
}
return aliceOrigSendToDevice(type, map);
};
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
macMethod = map
.get(alice.client.getUserId()!)!
.get(alice.client.deviceId!)!.message_authentication_code;
}
return bobOrigSendToDevice(type, map);
};
@@ -454,7 +465,7 @@ describe("SAS verification", function () {
);
};
alice.client.downloadKeys = () => {
return Promise.resolve({});
return Promise.resolve(new Map());
};
bob.client.crypto!.setDeviceVerification = jest.fn();
@@ -472,7 +483,7 @@ describe("SAS verification", function () {
return "bob+base64+ed25519+key";
};
bob.client.downloadKeys = () => {
return Promise.resolve({});
return Promise.resolve(new Map());
};
aliceSasEvent = null;
+5 -5
View File
@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
import { Room, RoomEvent } from "../../../../src/models/room";
import { logger } from "../../../../src/logger";
import { MatrixClient, ClientEvent, ICreateClientOpts } from "../../../../src/client";
import { MatrixClient, ClientEvent, ICreateClientOpts, SendToDeviceContentMap } from "../../../../src/client";
interface UserInfo {
userId: string;
@@ -36,16 +36,16 @@ export async function makeTestClients(
const clientMap: Record<string, Record<string, MatrixClient>> = {};
const makeSendToDevice =
(matrixClient: MatrixClient): MatrixClient["sendToDevice"] =>
async (type, map) => {
async (type: string, contentMap: SendToDeviceContentMap) => {
// logger.log(this.getUserId(), "sends", type, map);
for (const [userId, devMap] of Object.entries(map)) {
for (const [userId, deviceMessages] of contentMap) {
if (userId in clientMap) {
for (const [deviceId, msg] of Object.entries(devMap)) {
for (const [deviceId, message] of deviceMessages) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: matrixClient.getUserId()!,
type: type,
content: msg,
content: message,
});
const client = clientMap[userId][deviceId];
const decryptionPromise = event.isEncrypted()
@@ -25,6 +25,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
import { MapWithDefault } from "../../../../src/utils";
type MockClient = MatrixClient & {
popEvents: () => MatrixEvent[];
@@ -33,7 +34,9 @@ type MockClient = MatrixClient & {
function makeMockClient(userId: string, deviceId: string): MockClient {
let counter = 1;
let events: MatrixEvent[] = [];
const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
const deviceEvents: MapWithDefault<string, MapWithDefault<string, MatrixEvent[]>> = new MapWithDefault(
() => new MapWithDefault(() => []),
);
return {
getUserId() {
return userId;
@@ -58,15 +61,11 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
return Promise.resolve({ event_id: eventId });
},
sendToDevice(type: string, msgMap: Record<string, Record<string, IContent>>) {
for (const userId of Object.keys(msgMap)) {
const deviceMap = msgMap[userId];
for (const deviceId of Object.keys(deviceMap)) {
const content = deviceMap[deviceId];
sendToDevice(type: string, msgMap: Map<string, Map<string, IContent>>) {
for (const [userId, deviceMessages] of msgMap) {
for (const [deviceId, content] of deviceMessages) {
const event = new MatrixEvent({ content, type });
deviceEvents[userId] = deviceEvents[userId] || {};
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
deviceEvents[userId][deviceId].push(event);
deviceEvents.getOrCreate(userId).getOrCreate(deviceId).push(event);
}
}
return Promise.resolve({});
@@ -79,14 +78,9 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
return e;
},
// @ts-ignore special testing fn
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
const forDevice = deviceEvents[userId];
const events = forDevice && forDevice[deviceId];
const result = events || [];
if (events) {
delete forDevice[deviceId];
}
const result = deviceEvents.get(userId)?.get(deviceId) || [];
deviceEvents?.get(userId)?.delete(deviceId);
return result;
},
} as unknown as MockClient;
+10 -5
View File
@@ -204,9 +204,14 @@ describe("RoomWidgetClient", () => {
});
describe("to-device messages", () => {
const unencryptedContentMap = {
"@alice:example.org": { "*": { hello: "alice!" } },
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
const unencryptedContentMap = new Map([
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
]);
const expectedRequestData = {
["@alice:example.org"]: { ["*"]: { hello: "alice!" } },
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
};
it("sends unencrypted (sendToDevice)", async () => {
@@ -214,7 +219,7 @@ describe("RoomWidgetClient", () => {
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
await client.sendToDevice("org.example.foo", unencryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
});
it("sends unencrypted (queueToDevice)", async () => {
@@ -229,7 +234,7 @@ describe("RoomWidgetClient", () => {
],
};
await client.queueToDevice(batch);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
});
it("sends encrypted (encryptAndSendToDevices)", async () => {
+2 -2
View File
@@ -179,7 +179,7 @@ describe("EventTimelineSet", () => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
toStartOfTimeline: true,
});
}).toThrowError();
}).toThrow();
});
it("should not add a threaded reply to the main room timeline", () => {
@@ -399,7 +399,7 @@ describe("EventTimelineSet", () => {
it("should throw if timeline set has no room", () => {
const eventTimelineSet = new EventTimelineSet(undefined, {}, client);
expect(() => eventTimelineSet.canContain(messageEvent)).toThrowError();
expect(() => eventTimelineSet.canContain(messageEvent)).toThrow();
});
it("should return false if timeline set is for thread but event is not threaded", () => {
@@ -150,26 +150,6 @@ describe("PollResponseEvent", () => {
expect(response.spoiled).toBe(true);
});
it("should spoil the vote when answers are empty", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: [],
},
},
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(true);
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(true);
});
it("should spoil the vote when answers are not strings", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
-3
View File
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import DOMException from "domexception";
import { mocked } from "jest-mock";
import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src";
@@ -33,8 +32,6 @@ describe("MatrixHttpApi", () => {
const DONE = 0;
global.DOMException = DOMException;
beforeEach(() => {
xhr = {
upload: {} as XMLHttpRequestUpload,
+15 -44
View File
@@ -78,11 +78,11 @@ describe("InteractiveAuth", () => {
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
expect(doRequest).toHaveBeenCalledTimes(1);
expect(stateUpdated).toHaveBeenCalledTimes(1);
});
it("should handle auth errcode presence ", async () => {
it("should handle auth errcode presence", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
@@ -128,8 +128,8 @@ describe("InteractiveAuth", () => {
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
expect(doRequest).toHaveBeenCalledTimes(1);
expect(stateUpdated).toHaveBeenCalledTimes(1);
});
it("should handle set emailSid for email flow", async () => {
@@ -180,9 +180,9 @@ describe("InteractiveAuth", () => {
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
expect(requestEmailToken).toBeCalledTimes(0);
expect(doRequest).toHaveBeenCalledTimes(1);
expect(stateUpdated).toHaveBeenCalledTimes(1);
expect(requestEmailToken).toHaveBeenCalledTimes(0);
expect(ia.getEmailSid()).toBe("myEmailSid");
});
@@ -244,8 +244,8 @@ describe("InteractiveAuth", () => {
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
expect(doRequest).toHaveBeenCalledTimes(2);
expect(stateUpdated).toHaveBeenCalledTimes(1);
});
it("should make a request if authdata is null", async () => {
@@ -306,8 +306,8 @@ describe("InteractiveAuth", () => {
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
expect(doRequest).toHaveBeenCalledTimes(2);
expect(stateUpdated).toHaveBeenCalledTimes(1);
});
it("should start an auth stage and reject if no auth flow", async () => {
@@ -430,8 +430,8 @@ describe("InteractiveAuth", () => {
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(0);
expect(doRequest).toHaveBeenCalledTimes(1);
expect(stateUpdated).toHaveBeenCalledTimes(0);
});
describe("requestEmailToken", () => {
@@ -464,35 +464,6 @@ describe("InteractiveAuth", () => {
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
});
it("increases auth attempts", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
});
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
requestEmailToken.mockClear();
await ia.requestEmailToken();
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
});
it("passes errors through", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
@@ -508,7 +479,7 @@ describe("InteractiveAuth", () => {
requestEmailToken,
});
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrow("unspecific network error");
});
it("only starts one request at a time", async () => {
+324 -100
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 { Mocked, mocked } from "jest-mock";
import { logger } from "../../src/logger";
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
@@ -50,6 +50,12 @@ import {
MatrixScheduler,
Method,
Room,
EventTimelineSet,
PushRuleActionName,
TweakName,
RuleId,
IPushRule,
ConditionKind,
} from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
@@ -63,6 +69,7 @@ import { QueryDict } from "../../src/utils";
import { SyncState } from "../../src/sync";
import * as featureUtils from "../../src/feature";
import { StubStore } from "../../src/store/stub";
import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from "../../src/secret-storage";
jest.useFakeTimers();
@@ -725,6 +732,7 @@ describe("MatrixClient", function () {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
/* eslint-disable jest/no-conditional-expect */
if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual("");
return new MatrixEvent({
@@ -743,6 +751,7 @@ describe("MatrixClient", function () {
} else {
throw new Error("Unexpected event type or state key");
}
/* eslint-enable jest/no-conditional-expect */
},
} as Room["currentState"],
} as unknown as Room;
@@ -785,6 +794,7 @@ describe("MatrixClient", function () {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
/* eslint-disable jest/no-conditional-expect */
if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual("");
return new MatrixEvent({
@@ -803,6 +813,7 @@ describe("MatrixClient", function () {
} else {
throw new Error("Unexpected event type or state key");
}
/* eslint-enable jest/no-conditional-expect */
},
} as Room["currentState"],
} as unknown as Room;
@@ -820,6 +831,7 @@ describe("MatrixClient", function () {
getMyMembership: () => "join",
currentState: {
getStateEvents: (eventType, stateKey) => {
/* eslint-disable jest/no-conditional-expect */
if (eventType === EventType.RoomCreate) {
expect(stateKey).toEqual("");
return new MatrixEvent({
@@ -837,6 +849,7 @@ describe("MatrixClient", function () {
} else {
throw new Error("Unexpected event type or state key");
}
/* eslint-enable jest/no-conditional-expect */
},
} as Room["currentState"],
} as unknown as Room;
@@ -858,6 +871,7 @@ describe("MatrixClient", function () {
const syncPromise = new Promise<void>((resolve, reject) => {
client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "SYNCING") {
// eslint-disable-next-line jest/no-conditional-expect
expect(httpLookups.length).toEqual(0);
client.removeListener(ClientEvent.Sync, syncListener);
resolve();
@@ -893,7 +907,7 @@ describe("MatrixClient", function () {
describe("getOrCreateFilter", function () {
it("should POST createFilter if no id is present in localStorage", function () {});
it("should use an existing filter if id is present in localStorage", function () {});
it("should handle localStorage filterId missing from the server", function (done) {
it("should handle localStorage filterId missing from the server", async () => {
function getFilterName(userId: string, suffix?: string) {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
@@ -919,10 +933,8 @@ describe("MatrixClient", function () {
client.store.setFilterIdByName(filterName, invalidFilterId);
const filter = new Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function (filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
done();
});
const filterId = await client.getOrCreateFilter(filterName, filter);
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
});
});
@@ -933,7 +945,7 @@ describe("MatrixClient", function () {
expect(client.retryImmediately()).toBe(false);
});
it("should work on /filter", function (done) {
it("should work on /filter", async () => {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
@@ -944,23 +956,28 @@ describe("MatrixClient", function () {
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(2);
expect(client.retryImmediately()).toBe(true);
jest.advanceTimersByTime(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener(ClientEvent.Sync, syncListener);
done();
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
const wasPreparedPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, function syncListener(state) {
/* eslint-disable jest/no-conditional-expect */
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(2);
expect(client.retryImmediately()).toBe(true);
jest.advanceTimersByTime(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener(ClientEvent.Sync, syncListener);
resolve(null);
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
/* eslint-enable jest/no-conditional-expect */
});
});
client.startClient();
await client.startClient();
await wasPreparedPromise;
});
it("should work on /sync", function (done) {
it("should work on /sync", async () => {
httpLookups.push({
method: "GET",
path: "/sync",
@@ -972,22 +989,27 @@ describe("MatrixClient", function () {
data: SYNC_DATA,
});
client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(true);
jest.advanceTimersByTime(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
jest.advanceTimersByTime(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener(ClientEvent.Sync, syncListener);
done();
}
const isSyncingPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
/* eslint-disable jest/no-conditional-expect */
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(true);
/* eslint-enable jest/no-conditional-expect */
jest.advanceTimersByTime(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
jest.advanceTimersByTime(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener(ClientEvent.Sync, syncListener);
resolve(null);
}
});
});
client.startClient();
await client.startClient();
await isSyncingPromise;
});
it("should work on /pushrules", function (done) {
it("should work on /pushrules", async () => {
httpLookups = [];
httpLookups.push({
method: "GET",
@@ -998,20 +1020,25 @@ describe("MatrixClient", function () {
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on(ClientEvent.Sync, function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(3);
expect(client.retryImmediately()).toBe(true);
jest.advanceTimersByTime(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener(ClientEvent.Sync, syncListener);
done();
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
const wasPreparedPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, function syncListener(state) {
/* eslint-disable jest/no-conditional-expect */
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(3);
expect(client.retryImmediately()).toBe(true);
jest.advanceTimersByTime(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener(ClientEvent.Sync, syncListener);
resolve(null);
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
/* eslint-enable jest/no-conditional-expect */
});
});
client.startClient();
await client.startClient();
await wasPreparedPromise;
});
});
@@ -1035,14 +1062,17 @@ describe("MatrixClient", function () {
};
}
it("should transition null -> PREPARED after the first /sync", function (done) {
it("should transition null -> PREPARED after the first /sync", async () => {
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
const didSyncPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
await didSyncPromise;
});
it("should transition null -> ERROR after a failed /filter", function (done) {
it("should transition null -> ERROR after a failed /filter", async () => {
const expectedStates: [string, string | null][] = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
@@ -1052,14 +1082,17 @@ describe("MatrixClient", function () {
error: { errcode: "NOPE_NOPE_NOPE" },
});
expectedStates.push(["ERROR", null]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
const didSyncPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
await didSyncPromise;
});
// Disabled because now `startClient` makes a legit call to `/versions`
// And those tests are really unhappy about it... Not possible to figure
// out what a good resolution would look like
xit("should transition ERROR -> CATCHUP after /sync if prev failed", function (done) {
it.skip("should transition ERROR -> CATCHUP after /sync if prev failed", async () => {
const expectedStates: [string, string | null][] = [];
acceptKeepalives = false;
httpLookups = [];
@@ -1089,19 +1122,25 @@ describe("MatrixClient", function () {
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
const didSyncPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
await didSyncPromise;
});
it("should transition PREPARED -> SYNCING after /sync", function (done) {
it("should transition PREPARED -> SYNCING after /sync", async () => {
const expectedStates: [string, string | null][] = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
const didSyncPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
await didSyncPromise;
});
xit("should transition SYNCING -> ERROR after a failed /sync", function (done) {
it.skip("should transition SYNCING -> ERROR after a failed /sync", async () => {
acceptKeepalives = false;
const expectedStates: [string, string | null][] = [];
httpLookups.push({
@@ -1119,11 +1158,14 @@ describe("MatrixClient", function () {
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
const didSyncPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
await didSyncPromise;
});
xit("should transition ERROR -> SYNCING after /sync if prev failed", function (done) {
it.skip("should transition ERROR -> SYNCING after /sync if prev failed", async () => {
const expectedStates: [string, string | null][] = [];
httpLookups.push({
method: "GET",
@@ -1135,11 +1177,14 @@ describe("MatrixClient", function () {
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
const didSyncPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
await didSyncPromise;
});
it("should transition SYNCING -> SYNCING on subsequent /sync successes", function (done) {
it("should transition SYNCING -> SYNCING on subsequent /sync successes", async () => {
const expectedStates: [string, string | null][] = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -1147,11 +1192,14 @@ describe("MatrixClient", function () {
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
const didSyncPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
await didSyncPromise;
});
xit("should transition ERROR -> ERROR if keepalive keeps failing", function (done) {
it.skip("should transition ERROR -> ERROR if keepalive keeps failing", async () => {
acceptKeepalives = false;
const expectedStates: [string, string | null][] = [];
httpLookups.push({
@@ -1175,8 +1223,11 @@ describe("MatrixClient", function () {
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]);
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
client.startClient();
const didSyncPromise = new Promise((resolve) => {
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
});
await client.startClient();
await didSyncPromise;
});
});
@@ -1214,7 +1265,7 @@ describe("MatrixClient", function () {
expect(httpLookups.length).toBe(0);
});
xit("should be able to peek into a room using peekInRoom", function (done) {});
it.skip("should be able to peek into a room using peekInRoom", function () {});
});
describe("getPresence", function () {
@@ -1334,7 +1385,7 @@ describe("MatrixClient", function () {
client.redactEvent(roomId, eventId, txnId, {
with_relations: [RelationType.Reference],
});
}).toThrowError(
}).toThrow(
new Error(
"Server does not support relation based redactions " +
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId null`,
@@ -1464,7 +1515,7 @@ describe("MatrixClient", function () {
{ startOpts: { threadSupport: false }, hasThreadSupport: false },
{ startOpts: { experimentalThreadSupport: true }, hasThreadSupport: true },
{ startOpts: { experimentalThreadSupport: true, threadSupport: false }, hasThreadSupport: false },
])("enabled thread support for the SDK instance ", async ({ startOpts, hasThreadSupport }) => {
])("enabled thread support for the SDK instance", async ({ startOpts, hasThreadSupport }) => {
await client.startClient(startOpts);
expect(client.supportsThreads()).toBe(hasThreadSupport);
});
@@ -2410,31 +2461,6 @@ describe("MatrixClient", function () {
expect(rooms).toContain(room1);
expect(rooms).toContain(room2);
});
it("Ignores m.predecessor if we don't ask to use it", () => {
// Given 6 rooms, 2 of which have been replaced, and 2 of which WERE
// replaced by create events, but are now NOT replaced, because an
// m.predecessor event has changed the room's predecessor.
const {
room1,
room2,
replacedByCreate1,
replacedByCreate2,
replacedByDynamicPredecessor1,
replacedByDynamicPredecessor2,
} = setUpReplacedRooms();
// When we ask for the visible rooms
const rooms = client.getVisibleRooms(); // Don't supply msc3946ProcessDynamicPredecessor
// Then we only get the ones that have not been replaced
expect(rooms).not.toContain(replacedByCreate1);
expect(rooms).not.toContain(replacedByCreate2);
expect(rooms).toContain(replacedByDynamicPredecessor1);
expect(rooms).toContain(replacedByDynamicPredecessor2);
expect(rooms).toContain(room1);
expect(rooms).toContain(room2);
});
});
describe("getRoomUpgradeHistory", () => {
@@ -2619,7 +2645,7 @@ describe("MatrixClient", function () {
expect(history.map((room) => room.roomId)).toEqual([room1.roomId]);
});
it("Without verify links, includes predecessors that don't point forwards", () => {
it("Without verify links, includes successors that don't point backwards", () => {
// Given predecessors point forwards with tombstones, but
// successors do not point back with create events.
const [room1, room2, room3, room4] = createRoomHistory(false, true);
@@ -2685,4 +2711,202 @@ describe("MatrixClient", function () {
});
});
});
// these wrappers are deprecated, but we need coverage of them to pass the quality gate
describe("SecretStorage wrappers", () => {
let mockSecretStorage: Mocked<ServerSideSecretStorageImpl>;
beforeEach(() => {
mockSecretStorage = {
getDefaultKeyId: jest.fn(),
hasKey: jest.fn(),
isStored: jest.fn(),
} as unknown as Mocked<ServerSideSecretStorageImpl>;
client["_secretStorage"] = mockSecretStorage;
});
it("hasSecretStorageKey", async () => {
mockSecretStorage.hasKey.mockResolvedValue(false);
expect(await client.hasSecretStorageKey("mykey")).toBe(false);
expect(mockSecretStorage.hasKey).toHaveBeenCalledWith("mykey");
});
it("isSecretStored", async () => {
const mockResult = { key: {} as SecretStorageKeyDescriptionAesV1 };
mockSecretStorage.isStored.mockResolvedValue(mockResult);
expect(await client.isSecretStored("mysecret")).toBe(mockResult);
expect(mockSecretStorage.isStored).toHaveBeenCalledWith("mysecret");
});
it("getDefaultSecretStorageKeyId", async () => {
mockSecretStorage.getDefaultKeyId.mockResolvedValue("bzz");
expect(await client.getDefaultSecretStorageKeyId()).toEqual("bzz");
});
it("isKeyBackupKeyStored", async () => {
mockSecretStorage.isStored.mockResolvedValue(null);
expect(await client.isKeyBackupKeyStored()).toBe(null);
expect(mockSecretStorage.isStored).toHaveBeenCalledWith("m.megolm_backup.v1");
});
});
describe("paginateEventTimeline()", () => {
describe("notifications timeline", () => {
const unsafeNotification = {
actions: ["notify"],
room_id: "__proto__",
event: testUtils.mkMessage({
user: "@villain:server.org",
room: "!roomId:server.org",
msg: "I am nefarious",
}),
profile_tag: null,
read: true,
ts: 12345,
};
const goodNotification = {
actions: ["notify"],
room_id: "!favouriteRoom:server.org",
event: new MatrixEvent({
sender: "@bob:server.org",
room_id: "!roomId:server.org",
type: "m.call.invite",
content: {},
}),
profile_tag: null,
read: true,
ts: 12345,
};
const highlightNotification = {
actions: ["notify", { set_tweak: "highlight", value: true }],
room_id: "!roomId:server.org",
event: testUtils.mkMessage({
user: "@bob:server.org",
room: "!roomId:server.org",
msg: "I am highlighted banana",
}),
profile_tag: null,
read: true,
ts: 12345,
};
const setNotifsResponse = (notifications: any[] = []): void => {
const response: HttpLookup = {
method: "GET",
path: "/notifications",
data: { notifications: JSON.parse(JSON.stringify(notifications)) },
};
httpLookups = [response];
};
const callRule: IPushRule = {
actions: [PushRuleActionName.Notify],
conditions: [
{
kind: ConditionKind.EventMatch,
key: "type",
pattern: "m.call.invite",
},
],
default: true,
enabled: true,
rule_id: ".m.rule.call",
};
const masterRule: IPushRule = {
actions: [PushRuleActionName.DontNotify],
conditions: [],
default: true,
enabled: false,
rule_id: RuleId.Master,
};
const bananaRule = {
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
pattern: "banana",
rule_id: "banana",
default: false,
enabled: true,
} as IPushRule;
const pushRules = {
global: {
underride: [callRule],
override: [masterRule],
content: [bananaRule],
},
};
beforeEach(() => {
makeClient();
// this is how notif timeline is set up in react-sdk
const notifTimelineSet = new EventTimelineSet(undefined, {
timelineSupport: true,
pendingEvents: false,
});
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);
client.setNotifTimelineSet(notifTimelineSet);
setNotifsResponse();
client.setPushRules(pushRules);
});
it("should throw when trying to paginate forwards", async () => {
const timeline = client.getNotifTimelineSet()!.getLiveTimeline();
await expect(
async () => await client.paginateEventTimeline(timeline, { backwards: false }),
).rejects.toThrow("paginateNotifTimeline can only paginate backwards");
});
it("defaults limit to 30 events", async () => {
jest.spyOn(client.http, "authedRequest");
const timeline = client.getNotifTimelineSet()!.getLiveTimeline();
await client.paginateEventTimeline(timeline, { backwards: true });
expect(client.http.authedRequest).toHaveBeenCalledWith(Method.Get, "/notifications", {
limit: "30",
only: "highlight",
});
});
it("filters out unsafe notifications", async () => {
setNotifsResponse([unsafeNotification, goodNotification, highlightNotification]);
const timelineSet = client.getNotifTimelineSet()!;
const timeline = timelineSet.getLiveTimeline();
await client.paginateEventTimeline(timeline, { backwards: true });
// badNotification not added to timeline
const timelineEvents = timeline.getEvents();
expect(timelineEvents.length).toEqual(2);
});
it("sets push details on events and add to timeline", async () => {
setNotifsResponse([goodNotification, highlightNotification]);
const timelineSet = client.getNotifTimelineSet()!;
const timeline = timelineSet.getLiveTimeline();
await client.paginateEventTimeline(timeline, { backwards: true });
const [highlightEvent, goodEvent] = timeline.getEvents();
expect(highlightEvent.getPushActions()).toEqual({
notify: true,
tweaks: {
highlight: true,
},
});
expect(highlightEvent.getPushDetails().rule).toEqual({
...bananaRule,
kind: "content",
});
expect(goodEvent.getPushActions()).toEqual({
notify: true,
tweaks: {
highlight: false,
},
});
});
});
});
});
+2
View File
@@ -265,11 +265,13 @@ describe("MSC3089Branch", () => {
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
expect(content).toMatchObject({
retained: true, // canary for copying state
active: false,
});
} else if (stateKey === fileEventId2) {
// eslint-disable-next-line jest/no-conditional-expect
expect(content).toMatchObject({
active: true,
version: 2,
+12 -20
View File
@@ -130,14 +130,8 @@ describe("MSC3089TreeSpace", () => {
return Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN", error: "Sample Failure" }));
});
client.invite = fn;
try {
await tree.invite(target, false, false);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect((<MatrixError>e).errcode).toEqual("M_FORBIDDEN");
}
await expect(tree.invite(target, false, false)).rejects.toThrow("MatrixError: Sample Failure");
expect(fn).toHaveBeenCalledTimes(1);
});
@@ -357,13 +351,18 @@ describe("MSC3089TreeSpace", () => {
.fn()
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
expect([tree.roomId, subspaceId]).toContain(roomId);
let expectedType: string;
let expectedStateKey: string;
if (roomId === subspaceId) {
expect(eventType).toEqual(EventType.SpaceParent);
expect(stateKey).toEqual(tree.roomId);
expectedType = EventType.SpaceParent;
expectedStateKey = tree.roomId;
} else {
expect(eventType).toEqual(EventType.SpaceChild);
expect(stateKey).toEqual(subspaceId);
expectedType = EventType.SpaceChild;
expectedStateKey = subspaceId;
}
expect(eventType).toEqual(expectedType);
expect(stateKey).toEqual(expectedStateKey);
expect(content).toMatchObject({ via: [domain] });
// return value not used
@@ -629,15 +628,8 @@ describe("MSC3089TreeSpace", () => {
});
it("should throw when setting an order at the top level space", async () => {
try {
// The tree is what we've defined as top level, so it should work
await tree.setOrder(2);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect((<Error>e).message).toEqual("Cannot set order of top level spaces currently");
}
// The tree is what we've defined as top level, so it should work
await expect(tree.setOrder(2)).rejects.toThrow("Cannot set order of top level spaces currently");
});
it("should return a stable order for unordered children", () => {
+146 -1
View File
@@ -16,7 +16,8 @@ limitations under the License.
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils";
import { Crypto } from "../../../src/crypto";
import { Crypto, IEventDecryptionResult } from "../../../src/crypto";
import { IAnnotatedPushRule, PushRuleActionName, TweakName } from "../../../src";
describe("MatrixEvent", () => {
it("should create copies of itself", () => {
@@ -126,12 +127,31 @@ describe("MatrixEvent", () => {
expect(encryptedEvent.isEncrypted()).toBeTruthy();
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
expect(encryptedEvent.getContent()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: Error: test error **",
});
});
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
const crypto = {
decryptEvent: jest
.fn()
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.isEncrypted()).toBeTruthy();
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeTruthy();
expect(encryptedEvent.getContent()).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");
@@ -163,4 +183,129 @@ describe("MatrixEvent", () => {
expect(encryptedEvent.getType()).toEqual("m.room.message");
});
});
describe("replyEventId", () => {
it("should ignore 'm.relates_to' from encrypted content even if cleartext lacks one", async () => {
const eventId = "test_encrypted_event";
const encryptedEvent = new MatrixEvent({
event_id: eventId,
type: "m.room.encrypted",
content: {
ciphertext: "secrets",
},
});
const crypto = {
decryptEvent: jest.fn().mockImplementationOnce(() => {
return Promise.resolve<IEventDecryptionResult>({
clearEvent: {
type: "m.room.message",
content: {
"m.relates_to": {
"m.in_reply_to": {
event_id: "!anotherEvent",
},
},
},
},
});
}),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.getType()).toEqual("m.room.message");
expect(encryptedEvent.replyEventId).toBeUndefined();
});
});
describe("push details", () => {
const pushRule = {
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight, value: true }],
pattern: "banana",
rule_id: "banana",
kind: "override",
default: false,
enabled: true,
} as IAnnotatedPushRule;
describe("setPushActions()", () => {
it("sets actions on event", () => {
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushActions(actions);
expect(event.getPushActions()).toBe(actions);
});
it("sets actions to undefined", () => {
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushActions(null);
// undefined is set on state
expect(event.getPushDetails().actions).toBe(undefined);
// but pushActions getter returns null when falsy
expect(event.getPushActions()).toBe(null);
});
it("clears existing push rule", () => {
const prevActions = { notify: true, tweaks: { highlight: true } };
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushDetails(prevActions, pushRule);
event.setPushActions(actions);
// rule is not in event push cache
expect(event.getPushDetails()).toEqual({ actions });
});
});
describe("setPushDetails()", () => {
it("sets actions and rule on event", () => {
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushDetails(actions, pushRule);
expect(event.getPushDetails()).toEqual({
actions,
rule: pushRule,
});
});
it("clears existing push rule", () => {
const prevActions = { notify: true, tweaks: { highlight: true } };
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushDetails(prevActions, pushRule);
event.setPushActions(actions);
// rule is not in event push cache
expect(event.getPushDetails()).toEqual({ actions });
});
});
});
});
+82 -88
View File
@@ -14,12 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IEvent, MatrixEvent, PollEvent, Room } from "../../../src";
import { M_POLL_START } from "matrix-events-sdk";
import { EventType, IEvent, MatrixEvent, PollEvent, Room } from "../../../src";
import { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE } from "../../../src/@types/polls";
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
import { Poll } from "../../../src/models/poll";
import { isPollEvent, Poll } from "../../../src/models/poll";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
import { flushPromises } from "../../test-utils/flushPromises";
import { mkEvent } from "../../test-utils/test-utils";
jest.useFakeTimers();
@@ -27,6 +31,7 @@ describe("Poll", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
decryptEventIfNeeded: jest.fn().mockResolvedValue(true),
relations: jest.fn(),
});
const roomId = "!room:server";
@@ -74,13 +79,14 @@ describe("Poll", () => {
expect(poll.pollId).toEqual(basePollStartEvent.getId());
expect(poll.pollEvent).toEqual(basePollStartEvent.unstableExtensibleEvent);
expect(poll.isEnded).toBe(false);
expect(poll.endEventId).toBe(undefined);
});
it("throws when poll start has no room id", () => {
const pollStartEvent = new MatrixEvent(
PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
);
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrowError("Invalid poll start event.");
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
});
it("throws when poll start has no event id", () => {
@@ -88,7 +94,7 @@ describe("Poll", () => {
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
room_id: roomId,
});
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrowError("Invalid poll start event.");
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
});
describe("fetching responses", () => {
@@ -130,7 +136,7 @@ describe("Poll", () => {
});
it("filters relations for relevent response events", async () => {
const replyEvent = new MatrixEvent({ type: "m.room.message" });
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
@@ -172,6 +178,8 @@ describe("Poll", () => {
jest.spyOn(poll, "emit");
const responses = await poll.getResponses();
await flushPromises();
expect(mockClient.relations.mock.calls).toEqual([
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: undefined }],
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-1" }],
@@ -184,6 +192,47 @@ describe("Poll", () => {
});
});
describe("undecryptable relations", () => {
it("counts undecryptable relation events when getting responses", async () => {
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);
mockClient.relations.mockResolvedValue({
events: [replyEvent, stableResponseEvent, undecryptableEvent],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
await poll.getResponses();
expect(poll.undecryptableRelationsCount).toBe(1);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 1);
});
it("adds to undercryptable event count when new relation is undecryptable", async () => {
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
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);
mockClient.relations.mockResolvedValue({
events: [replyEvent, stableResponseEvent, undecryptableEvent],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
await poll.getResponses();
expect(poll.undecryptableRelationsCount).toBe(1);
await poll.onNewRelation(undecryptableEvent2);
expect(poll.undecryptableRelationsCount).toBe(2);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 2);
});
});
describe("with poll end event", () => {
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" });
const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" });
@@ -204,6 +253,7 @@ describe("Poll", () => {
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
expect(poll.isEnded).toBe(true);
expect(poll.endEventId).toBe(stablePollEndEvent.getId()!);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
});
@@ -292,33 +342,6 @@ describe("Poll", () => {
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org");
});
it("does not set poll end event when an earlier end event already exists", async () => {
const earlierPollEndEvent = makeRelatedEvent(
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
now,
);
const laterPollEndEvent = makeRelatedEvent(
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
now + 2000,
);
const poll = new Poll(basePollStartEvent, mockClient, room);
await poll.getResponses();
poll.onNewRelation(earlierPollEndEvent);
// first end event set correctly
expect(poll.isEnded).toBeTruthy();
// reset spy count
jest.spyOn(poll, "emit").mockClear();
poll.onNewRelation(laterPollEndEvent);
// didn't set new end event, didn't refilter responses
expect(poll.emit).not.toHaveBeenCalled();
expect(poll.isEnded).toBeTruthy();
});
it("replaces poll end event and refilters when an older end event already exists", async () => {
const earlierPollEndEvent = makeRelatedEvent(
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
@@ -356,25 +379,6 @@ describe("Poll", () => {
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
});
it("does not set poll end event when sent by invalid user", async () => {
maySendRedactionForEventSpy.mockReturnValue(false);
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@charlie:server.org" });
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
mockClient.relations.mockResolvedValue({
events: [responseEventAfterEnd],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
await poll.getResponses();
jest.spyOn(poll, "emit");
poll.onNewRelation(stablePollEndEvent);
// didn't end, didn't refilter responses
expect(poll.emit).not.toHaveBeenCalled();
expect(poll.isEnded).toBeFalsy();
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org");
});
it("does not set poll end event when an earlier end event already exists", async () => {
const earlierPollEndEvent = makeRelatedEvent(
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
@@ -402,43 +406,6 @@ describe("Poll", () => {
expect(poll.isEnded).toBeTruthy();
});
it("replaces poll end event and refilters when an older end event already exists", async () => {
const earlierPollEndEvent = makeRelatedEvent(
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
now,
);
const laterPollEndEvent = makeRelatedEvent(
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
now + 2000,
);
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
mockClient.relations.mockResolvedValue({
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, laterPollEndEvent],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
const responses = await poll.getResponses();
// all responses have a timestamp < laterPollEndEvent
expect(responses.getRelations().length).toEqual(3);
// first end event set correctly
expect(poll.isEnded).toBeTruthy();
// reset spy count
jest.spyOn(poll, "emit").mockClear();
// add a valid end event with earlier timestamp
poll.onNewRelation(earlierPollEndEvent);
// emitted new end event
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
// filtered responses and emitted
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
});
it("sets poll end event and refilters responses based on timestamp", async () => {
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
@@ -489,4 +456,31 @@ describe("Poll", () => {
expect(responses.getRelations()).toEqual([responseEvent]);
});
});
describe("isPollEvent", () => {
it("should return »false« for a non-poll event", () => {
const messageEvent = mkEvent({
event: true,
type: EventType.RoomMessage,
content: {},
user: mockClient.getSafeUserId(),
room: room.roomId,
});
expect(isPollEvent(messageEvent)).toBe(false);
});
it.each([[M_POLL_START.name], [M_POLL_RESPONSE.name], [M_POLL_END.name]])(
"should return »true« for a »%s« event",
(type: string) => {
const pollEvent = mkEvent({
event: true,
type,
content: {},
user: mockClient.getSafeUserId(),
room: room.roomId,
});
expect(isPollEvent(pollEvent)).toBe(true);
},
);
});
});
+2
View File
@@ -82,6 +82,7 @@ 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),
@@ -193,6 +194,7 @@ 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),
+29 -3
View File
@@ -53,6 +53,7 @@ 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),
@@ -134,7 +135,7 @@ describe("fixNotificationCountOnDecryption", () => {
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(3);
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
@@ -154,11 +155,11 @@ describe("fixNotificationCountOnDecryption", () => {
fixNotificationCountOnDecryption(mockClient, threadEvent);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(2);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
});
it("does not change the room count when there's no unread count", () => {
it("does not change the thread count when there's no unread count", () => {
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
@@ -192,6 +193,31 @@ describe("fixNotificationCountOnDecryption", () => {
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
});
it("does not change the total room count when an event is marked as non-notifying", () => {
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
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));
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
});
it("does not change the total room count when a threaded event is marked as non-notifying", () => {
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));
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
});
it("emits events", () => {
const cb = jest.fn();
room.on(RoomEvent.UnreadNotifications, cb);
+303 -20
View File
@@ -1,6 +1,7 @@
import * as utils from "../test-utils/test-utils";
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName, RuleId } from "../../src";
import { mockClientMethodsUser } from "../test-utils/client";
describe("NotificationService", function () {
const testUserId = "@ali:matrix.org";
@@ -11,6 +12,23 @@ describe("NotificationService", function () {
let pushProcessor: PushProcessor;
const msc3914RoomCallRule = {
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: "call_started",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
};
// These would be better if individual rules were configured in the tests themselves.
const matrixClient = {
getRoom: function () {
@@ -28,9 +46,8 @@ describe("NotificationService", function () {
},
};
},
credentials: {
userId: testUserId,
},
...mockClientMethodsUser(testUserId),
supportsIntentionalMentions: () => true,
pushRules: {
device: {},
global: {
@@ -163,26 +180,11 @@ describe("NotificationService", function () {
enabled: true,
rule_id: ".m.rule.room_one_to_one",
},
{
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: "call_started",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
},
],
room: [],
sender: [],
underride: [
msc3914RoomCallRule,
{
actions: ["dont-notify"],
conditions: [
@@ -287,6 +289,13 @@ describe("NotificationService", function () {
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour.
//
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1 which
// describes pattern should glob:
//
// 1. * matches 0 or more characters;
// 2. ? matches exactly one character
it("should bing on character group ([abc]) bing words.", function () {
testEvent.event.content!.body = "Ping!";
let actions = pushProcessor.actionsForEvent(testEvent);
@@ -296,12 +305,14 @@ describe("NotificationService", function () {
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour. (See above.)
it("should bing on character range ([a-z]) bing words.", function () {
testEvent.event.content!.body = "I ate 6 pies";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour. (See above.)
it("should bing on character negation ([!a]) bing words.", function () {
testEvent.event.content!.body = "boke";
let actions = pushProcessor.actionsForEvent(testEvent);
@@ -330,6 +341,8 @@ describe("NotificationService", function () {
// invalid
it("should gracefully handle bad input.", function () {
// The following body is an object (not a string) and thus is invalid
// for matching against.
testEvent.event.content!.body = { foo: "bar" };
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
@@ -493,4 +506,274 @@ describe("NotificationService", function () {
});
});
});
describe("Test exact event matching", () => {
it.each([
// Simple string matching.
{ value: "bar", eventValue: "bar", expected: true },
// Matches are case-sensitive.
{ value: "bar", eventValue: "BAR", expected: false },
// Matches must match the full string.
{ value: "bar", eventValue: "barbar", expected: false },
// Values should not be type-coerced.
{ value: "bar", eventValue: true, expected: false },
{ value: "bar", eventValue: 1, expected: false },
{ value: "bar", eventValue: false, expected: false },
// Boolean matching.
{ value: true, eventValue: true, expected: true },
{ value: false, eventValue: false, expected: true },
// Types should not be coerced.
{ value: true, eventValue: "true", expected: false },
{ value: true, eventValue: 1, expected: false },
{ value: false, eventValue: null, expected: false },
// Null matching.
{ value: null, eventValue: null, expected: true },
// Types should not be coerced
{ value: null, eventValue: false, expected: false },
{ value: null, eventValue: 0, expected: false },
{ value: null, eventValue: "", expected: false },
{ value: null, eventValue: undefined, expected: false },
// Compound values should never be matched.
{ value: "bar", eventValue: ["bar"], expected: false },
{ value: "bar", eventValue: { bar: true }, expected: false },
{ value: true, eventValue: [true], expected: false },
{ value: true, eventValue: { true: true }, expected: false },
{ value: null, eventValue: [], expected: false },
{ value: null, eventValue: {}, expected: false },
])("test $value against $eventValue", ({ value, eventValue, expected }) => {
matrixClient.pushRules! = {
global: {
override: [
{
actions: [PushRuleActionName.Notify],
conditions: [
{
kind: ConditionKind.EventPropertyIs,
key: "content.foo",
value: value,
},
],
default: true,
enabled: true,
rule_id: ".m.rule.test",
},
],
},
};
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
user: "@alfred:localhost",
event: true,
content: {
foo: eventValue,
},
});
const actions = pushProcessor.actionsForEvent(testEvent);
expect(!!actions?.notify).toBe(expected);
});
});
describe("Test event property contains", () => {
it.each([
// Simple string matching.
{ value: "bar", eventValue: ["bar"], expected: true },
// Matches are case-sensitive.
{ value: "bar", eventValue: ["BAR"], expected: false },
// Values should not be type-coerced.
{ value: "bar", eventValue: [true], expected: false },
{ value: "bar", eventValue: [1], expected: false },
{ value: "bar", eventValue: [false], expected: false },
// Boolean matching.
{ value: true, eventValue: [true], expected: true },
{ value: false, eventValue: [false], expected: true },
// Types should not be coerced.
{ value: true, eventValue: ["true"], expected: false },
{ value: true, eventValue: [1], expected: false },
{ value: false, eventValue: [null], expected: false },
// Null matching.
{ value: null, eventValue: [null], expected: true },
// Types should not be coerced
{ value: null, eventValue: [false], expected: false },
{ value: null, eventValue: [0], expected: false },
{ value: null, eventValue: [""], expected: false },
{ value: null, eventValue: [undefined], expected: false },
// Non-array or empty values should never be matched.
{ value: "bar", eventValue: "bar", expected: false },
{ value: "bar", eventValue: { bar: true }, expected: false },
{ value: true, eventValue: { true: true }, expected: false },
{ value: true, eventValue: true, expected: false },
{ value: null, eventValue: [], expected: false },
{ value: null, eventValue: {}, expected: false },
{ value: null, eventValue: null, expected: false },
{ value: null, eventValue: undefined, expected: false },
])("test $value against $eventValue", ({ value, eventValue, expected }) => {
matrixClient.pushRules! = {
global: {
override: [
{
actions: [PushRuleActionName.Notify],
conditions: [
{
kind: ConditionKind.EventPropertyContains,
key: "content.foo",
value: value,
},
],
default: true,
enabled: true,
rule_id: ".m.rule.test",
},
],
},
};
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
user: "@alfred:localhost",
event: true,
content: {
foo: eventValue,
},
});
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions?.notify).toBe(expected ? true : undefined);
});
});
it.each([
// The properly escaped key works.
{ key: "content.m\\.test.foo", pattern: "bar", expected: true },
// An unescaped version does not match.
{ key: "content.m.test.foo", pattern: "bar", expected: false },
// Over escaping does not match.
{ key: "content.m\\.test\\.foo", pattern: "bar", expected: false },
// Escaping backslashes should match.
{ key: "content.m\\\\example", pattern: "baz", expected: true },
// An unnecessary escape sequence leaves the backslash and still matches.
{ key: "content.m\\example", pattern: "baz", expected: true },
])("test against escaped dotted paths '$key'", ({ key, pattern, expected }) => {
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
user: "@alfred:localhost",
event: true,
content: {
// A dot in the field name.
"m.test": { foo: "bar" },
// A backslash in a field name.
"m\\example": "baz",
},
});
expect(
pushProcessor.ruleMatchesEvent(
{
rule_id: "rule1",
actions: [],
conditions: [
{
kind: ConditionKind.EventMatch,
key: key,
pattern: pattern,
},
],
default: false,
enabled: true,
},
testEvent,
),
).toBe(expected);
});
describe("getPushRuleById()", () => {
it("returns null when rule id is not in rule set", () => {
expect(pushProcessor.getPushRuleById("non-existant-rule")).toBeNull();
});
it("returns push rule when it is found in rule set", () => {
expect(pushProcessor.getPushRuleById(".org.matrix.msc3914.rule.room.call")).toEqual(msc3914RoomCallRule);
});
});
describe("getPushRuleAndKindById()", () => {
it("returns null when rule id is not in rule set", () => {
expect(pushProcessor.getPushRuleAndKindById("non-existant-rule")).toBeNull();
});
it("returns push rule when it is found in rule set", () => {
expect(pushProcessor.getPushRuleAndKindById(".org.matrix.msc3914.rule.room.call")).toEqual({
kind: "underride",
rule: msc3914RoomCallRule,
});
});
});
describe("test intentional mentions behaviour", () => {
it.each([RuleId.ContainsUserName, RuleId.ContainsDisplayName, RuleId.AtRoomNotification])(
"Rule %s matches unless intentional mentions are enabled",
(ruleId) => {
const rule = {
rule_id: ruleId,
actions: [],
conditions: [],
default: false,
enabled: true,
};
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(true);
// Add the mentions property to the event and the rule is now disabled.
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
user: "@alfred:localhost",
event: true,
content: {
"body": "",
"msgtype": "m.text",
"org.matrix.msc3952.mentions": {},
},
});
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(false);
},
);
});
});
describe("Test PushProcessor.partsForDottedKey", function () {
it.each([
// A field with no dots.
["m", ["m"]],
// Simple dotted fields.
["m.foo", ["m", "foo"]],
["m.foo.bar", ["m", "foo", "bar"]],
// Backslash is used as an escape character.
["m\\.foo", ["m.foo"]],
["m\\\\.foo", ["m\\", "foo"]],
["m\\\\\\.foo", ["m\\.foo"]],
["m\\\\\\\\.foo", ["m\\\\", "foo"]],
["m\\foo", ["m\\foo"]],
["m\\\\foo", ["m\\foo"]],
["m\\\\\\foo", ["m\\\\foo"]],
["m\\\\\\\\foo", ["m\\\\foo"]],
// Ensure that escapes at the end don't cause issues.
["m.foo\\", ["m", "foo\\"]],
["m.foo\\\\", ["m", "foo\\"]],
["m.foo\\.", ["m", "foo."]],
["m.foo\\\\.", ["m", "foo\\", ""]],
["m.foo\\\\\\.", ["m", "foo\\."]],
// Empty parts (corresponding to properties which are an empty string) are allowed.
[".m", ["", "m"]],
["..m", ["", "", "m"]],
["m.", ["m", ""]],
["m..", ["m", "", ""]],
["m..foo", ["m", "", "foo"]],
])("partsFotDottedKey for %s", (path: string, expected: string[]) => {
expect(PushProcessor.partsForDottedKey(path)).toStrictEqual(expected);
});
});
@@ -16,7 +16,7 @@ limitations under the License.
import "../../olm-loader";
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels";
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
import { decodeBase64 } from "../../../src/crypto/olmlib";
import { DummyTransport } from "./DummyTransport";
@@ -24,7 +24,7 @@ function makeTransport(name: string) {
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
}
describe("ECDHv1", function () {
describe("ECDHv2", function () {
beforeAll(async function () {
await global.Olm.init();
});
@@ -37,9 +37,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -62,9 +62,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -87,9 +87,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -109,9 +109,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -135,9 +135,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -146,7 +146,7 @@ describe("ECDHv1", function () {
// send a message without encryption
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
expect(bob.receive()).rejects.toThrowError();
expect(bob.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
await bob.cancel(RendezvousFailureReason.Unknown);
@@ -159,12 +159,12 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
expect(alice.receive()).rejects.toThrowError();
expect(alice.receive()).rejects.toThrow();
await alice.cancel(RendezvousFailureReason.Unknown);
});
+62 -29
View File
@@ -19,9 +19,9 @@ import MockHttpBackend from "matrix-mock-request";
import "../../olm-loader";
import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import {
ECDHv1RendezvousCode,
ECDHv2RendezvousCode as ECDHRendezvousCode,
MSC3903ECDHPayload,
MSC3903ECDHv1RendezvousChannel,
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
} from "../../../src/rendezvous/channels";
import { MatrixClient } from "../../../src";
import {
@@ -38,6 +38,7 @@ function makeMockClient(opts: {
deviceId: string;
deviceKey?: string;
msc3882Enabled: boolean;
msc3882r0Only: boolean;
msc3886Enabled: boolean;
devices?: Record<string, Partial<DeviceInfo>>;
verificationFunction?: (
@@ -58,6 +59,17 @@ function makeMockClient(opts: {
},
};
},
getCapabilities() {
return opts.msc3882r0Only
? {}
: {
capabilities: {
"org.matrix.msc3882.get_login_token": {
enabled: opts.msc3882Enabled,
},
},
};
},
getUserId() {
return opts.userId;
},
@@ -111,6 +123,7 @@ describe("Rendezvous", function () {
deviceId: "DEVICEID",
msc3886Enabled: false,
msc3882Enabled: true,
msc3882r0Only: true,
});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
@@ -126,7 +139,7 @@ describe("Rendezvous", function () {
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
expect(aliceRz.code).toBeUndefined();
@@ -143,7 +156,7 @@ describe("Rendezvous", function () {
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256");
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
"https://fallbackserver/rz/123",
@@ -166,7 +179,13 @@ describe("Rendezvous", function () {
await aliceRz.close();
});
it("no protocols", async function () {
async function testNoProtocols({
msc3882Enabled,
msc3882r0Only,
}: {
msc3882Enabled: boolean;
msc3882r0Only: boolean;
}) {
const aliceTransport = makeTransport("Alice");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
@@ -178,14 +197,15 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: false,
msc3886Enabled: false,
msc3882Enabled,
msc3882r0Only,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -193,7 +213,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -218,9 +238,17 @@ describe("Rendezvous", function () {
await aliceStartProm;
await bobStartPromise;
}
it("no protocols - r0", async function () {
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: true });
});
it("new device declines protocol", async function () {
it("no protocols - r1", async function () {
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: false });
});
it("new device declines protocol with outcome unsupported", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
@@ -233,13 +261,14 @@ describe("Rendezvous", function () {
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -247,7 +276,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -278,7 +307,7 @@ describe("Rendezvous", function () {
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
});
it("new device declines protocol", async function () {
it("new device requests an invalid protocol", async function () {
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
transports.push(aliceTransport, bobTransport);
@@ -291,13 +320,14 @@ describe("Rendezvous", function () {
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -305,7 +335,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -349,13 +379,14 @@ describe("Rendezvous", function () {
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -363,7 +394,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -409,13 +440,14 @@ describe("Rendezvous", function () {
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -423,7 +455,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -477,6 +509,7 @@ describe("Rendezvous", function () {
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
devices,
deviceKey: "aaaa",
@@ -485,11 +518,11 @@ describe("Rendezvous", function () {
master: "mmmmm",
},
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -497,7 +530,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -570,7 +603,7 @@ describe("Rendezvous", function () {
it("device not online within timeout", async function () {
const { aliceRz } = await completeLogin({});
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("device appears online within timeout", async function () {
@@ -594,7 +627,7 @@ describe("Rendezvous", function () {
getFingerprint: () => "bbbb",
};
}, 1500);
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
});
it("mismatched device key", async function () {
@@ -603,6 +636,6 @@ describe("Rendezvous", function () {
getFingerprint: () => "XXXX",
},
});
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(/different key/);
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
});
});
@@ -98,7 +98,7 @@ describe("SimpleHttpRendezvousTransport", function () {
it("should throw an error when no server available", function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
expect(simpleHttpTransport.send({})).rejects.toThrowError("Invalid rendezvous URI");
expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
});
it("POST to fallback server", async function () {
@@ -130,7 +130,7 @@ describe("SimpleHttpRendezvousTransport", function () {
fetchFn,
});
const prom = simpleHttpTransport.send({});
expect(prom).rejects.toThrowError();
expect(prom).rejects.toThrow();
httpBackend.when("POST", "https://fallbackserver/rz").response = {
body: null,
response: {
@@ -163,15 +163,6 @@ describe("SimpleHttpRendezvousTransport", function () {
);
});
it("POST with relative path response including parent", async function () {
await postAndCheckLocation(
false,
"https://fallbackserver/rz/abc",
"../xyz/123",
"https://fallbackserver/rz/xyz/123",
);
});
it("POST to follow 307 to other server", async function () {
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
@@ -373,7 +364,7 @@ describe("SimpleHttpRendezvousTransport", function () {
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
expect(simpleHttpTransport.details()).rejects.toThrowError();
expect(simpleHttpTransport.details()).rejects.toThrow();
});
it("send after cancelled", async function () {
@@ -394,7 +385,7 @@ describe("SimpleHttpRendezvousTransport", function () {
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
expect(simpleHttpTransport.receive()).rejects.toThrowError();
expect(simpleHttpTransport.receive()).rejects.toThrow();
});
it("404 failure callback", async function () {
@@ -416,7 +407,7 @@ describe("SimpleHttpRendezvousTransport", function () {
},
};
await httpBackend.flush("", 1);
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Unknown);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Unknown);
});
it("404 failure callback mapped to expired", async function () {
@@ -456,7 +447,7 @@ describe("SimpleHttpRendezvousTransport", function () {
},
};
await httpBackend.flush("");
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Expired);
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired);
}
});
});
+23 -18
View File
@@ -25,6 +25,8 @@ import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@typ
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { M_BEACON } from "../../src/@types/beacon";
import { MatrixClient } from "../../src/client";
import { DecryptionError } from "../../src/crypto/algorithms";
import { defer } from "../../src/utils";
describe("RoomState", function () {
const roomId = "!foo:bar";
@@ -886,7 +888,7 @@ describe("RoomState", function () {
expect(emitSpy).not.toHaveBeenCalled();
});
it("adds locations to beacons", () => {
it("adds locations to beacons", async () => {
const location1 = makeBeaconEvent(userA, {
beaconInfoId: "$beacon1",
timestamp: Date.now() + 1,
@@ -906,7 +908,7 @@ describe("RoomState", function () {
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beaconInstance, "addLocations");
state.processBeaconEvents([location1, location2, location3], mockClient);
await state.processBeaconEvents([location1, location2, location3], mockClient);
expect(addLocationsSpy).toHaveBeenCalledTimes(2);
// only called with locations for beacon1
@@ -978,51 +980,50 @@ describe("RoomState", function () {
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
});
it("decrypts related events if needed", () => {
it("decrypts related events if needed", async () => {
const location = makeBeaconEvent(userA, {
beaconInfoId: beacon1.getId(),
});
state.setStateEvents([beacon1, beacon2]);
state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
await state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
// discard unrelated events early
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
});
it("listens for decryption on events that are being decrypted", () => {
it("awaits for decryption on events that are being decrypted", async () => {
const decryptingRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: beacon1RelationContent,
});
jest.spyOn(decryptingRelatedEvent, "isBeingDecrypted").mockReturnValue(true);
// spy on event.once
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, "once");
state.setStateEvents([beacon1, beacon2]);
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
await state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// listener was added
expect(eventOnceSpy).toHaveBeenCalled();
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalled();
});
it("listens for decryption on events that have decryption failure", () => {
it("listens for decryption on events that have decryption failure", async () => {
const failedDecryptionRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
content: beacon1RelationContent,
});
jest.spyOn(failedDecryptionRelatedEvent, "isDecryptionFailure").mockReturnValue(true);
mockClient.decryptEventIfNeeded.mockRejectedValue(new DecryptionError("ERR", "msg"));
// spy on event.once
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, "once");
const eventOnceSpy = jest.spyOn(failedDecryptionRelatedEvent, "once");
state.setStateEvents([beacon1, beacon2]);
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
await state.processBeaconEvents([failedDecryptionRelatedEvent], mockClient);
// listener was added
expect(eventOnceSpy).toHaveBeenCalled();
});
it("discard events that are not m.beacon type after decryption", () => {
it("discard events that are not m.beacon type after decryption", async () => {
const decryptingRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
@@ -1032,7 +1033,7 @@ describe("RoomState", function () {
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, "addLocations").mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
await state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// this event is a message after decryption
decryptingRelatedEvent.event.type = EventType.RoomMessage;
@@ -1041,7 +1042,7 @@ describe("RoomState", function () {
expect(addLocationsSpy).not.toHaveBeenCalled();
});
it("adds locations to beacons after decryption", () => {
it("adds locations to beacons after decryption", async () => {
const decryptingRelatedEvent = new MatrixEvent({
sender: userA,
type: EventType.RoomMessageEncrypted,
@@ -1051,16 +1052,20 @@ describe("RoomState", function () {
beaconInfoId: "$beacon1",
timestamp: Date.now() + 1,
});
jest.spyOn(decryptingRelatedEvent, "isBeingDecrypted").mockReturnValue(true);
const deferred = defer<void>();
mockClient.decryptEventIfNeeded.mockReturnValue(deferred.promise);
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, "addLocations").mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
const prom = state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// update type after '''decryption'''
decryptingRelatedEvent.event.type = M_BEACON.name;
decryptingRelatedEvent.event.content = locationEvent.event.content;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
deferred.resolve();
await prom;
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
});
+80 -31
View File
@@ -19,7 +19,7 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, PollStartEvent } from "matrix-events-sdk";
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, PollStartEvent } from "matrix-events-sdk";
import * as utils from "../test-utils/test-utils";
import { emitPromise } from "../test-utils/test-utils";
@@ -53,6 +53,7 @@ import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../..
import { Crypto } from "../../src/crypto";
import { mkThread } from "../test-utils/thread";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
import { logger } from "../../src/logger";
describe("Room", function () {
const roomId = "!foo:bar";
@@ -171,6 +172,8 @@ describe("Room", function () {
room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState");
// @ts-ignore
room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState");
jest.spyOn(logger, "warn");
});
describe("getCreator", () => {
@@ -390,36 +393,30 @@ describe("Room", function () {
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
const remoteEventId = remoteEvent.getId();
let callCount = 0;
room.on(RoomEvent.LocalEchoUpdated, (event, emitRoom, oldEventId, oldStatus) => {
switch (callCount) {
case 0:
expect(event.getId()).toEqual(localEventId);
expect(event.status).toEqual(EventStatus.SENDING);
expect(emitRoom).toEqual(room);
expect(oldEventId).toBeUndefined();
expect(oldStatus).toBeUndefined();
break;
case 1:
expect(event.getId()).toEqual(remoteEventId);
expect(event.status).toBeNull();
expect(emitRoom).toEqual(room);
expect(oldEventId).toEqual(localEventId);
expect(oldStatus).toBe(EventStatus.SENDING);
break;
}
callCount += 1;
});
const stub = jest.fn();
room.on(RoomEvent.LocalEchoUpdated, stub);
// first add the local echo
room.addPendingEvent(localEvent, "TXN_ID");
expect(room.timeline.length).toEqual(1);
expect(stub.mock.calls[0][0].getId()).toEqual(localEventId);
expect(stub.mock.calls[0][0].status).toEqual(EventStatus.SENDING);
expect(stub.mock.calls[0][1]).toEqual(room);
expect(stub.mock.calls[0][2]).toBeUndefined();
expect(stub.mock.calls[0][3]).toBeUndefined();
// then the remoteEvent
room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(1);
expect(callCount).toEqual(2);
expect(stub).toHaveBeenCalledTimes(2);
expect(stub.mock.calls[1][0].getId()).toEqual(remoteEventId);
expect(stub.mock.calls[1][0].status).toBeNull();
expect(stub.mock.calls[1][1]).toEqual(room);
expect(stub.mock.calls[1][2]).toEqual(localEventId);
expect(stub.mock.calls[1][3]).toBe(EventStatus.SENDING);
});
it("should be able to update local echo without a txn ID (/send then /sync)", function () {
@@ -788,8 +785,12 @@ describe("Room", function () {
});
};
describe("resetLiveTimeline with timeline support enabled", resetTimelineTests.bind(null, true));
describe("resetLiveTimeline with timeline support disabled", resetTimelineTests.bind(null, false));
describe("resetLiveTimeline with timeline support enabled", () => {
resetTimelineTests.bind(null, true);
});
describe("resetLiveTimeline with timeline support disabled", () => {
resetTimelineTests.bind(null, false);
});
describe("compareEventOrdering", function () {
beforeEach(function () {
@@ -3251,7 +3252,7 @@ describe("Room", function () {
return event;
};
it("adds poll models to room state for a poll start event ", async () => {
it("adds poll models to room state for a poll start event", async () => {
const pollStartEvent = makePollStart("1");
const events = [pollStartEvent];
@@ -3263,7 +3264,7 @@ describe("Room", function () {
expect(room.emit).toHaveBeenCalledWith(PollEvent.New, pollInstance);
});
it("adds related events to poll models", async () => {
it("adds related events to poll models and log errors", async () => {
const pollStartEvent = makePollStart("1");
const pollStartEvent2 = makePollStart("2");
const events = [pollStartEvent, pollStartEvent2];
@@ -3276,6 +3277,7 @@ describe("Room", function () {
},
},
});
const messageEvent = new MatrixEvent({
type: "m.room.messsage",
content: {
@@ -3283,6 +3285,19 @@ describe("Room", function () {
},
});
const errorEvent = new MatrixEvent({
type: M_POLL_START.name,
content: {
text: "Error!!!!",
},
});
const error = new Error("Test error");
mocked(client.decryptEventIfNeeded).mockImplementation(async (event: MatrixEvent) => {
if (event === errorEvent) throw error;
});
// init poll
await room.processPollEvents(events);
@@ -3291,7 +3306,7 @@ describe("Room", function () {
jest.spyOn(poll, "onNewRelation");
jest.spyOn(poll2, "onNewRelation");
await room.processPollEvents([pollResponseEvent, messageEvent]);
await room.processPollEvents([errorEvent, messageEvent, pollResponseEvent]);
// only called for relevant event
expect(poll.onNewRelation).toHaveBeenCalledTimes(1);
@@ -3299,6 +3314,32 @@ describe("Room", function () {
// only called on poll with relation
expect(poll2.onNewRelation).not.toHaveBeenCalled();
expect(logger.warn).toHaveBeenCalledWith("Error processing poll event", errorEvent.getId(), error);
});
it("should retry on decryption", async () => {
const pollStartEventId = "poll1";
const pollStartEvent = makePollStart(pollStartEventId);
// simulate decryption failure
const isDecryptionFailureSpy = jest.spyOn(pollStartEvent, "isDecryptionFailure").mockReturnValue(true);
await room.processPollEvents([pollStartEvent]);
// do not expect a poll to show up for the room
expect(room.polls.get(pollStartEventId)).toBeUndefined();
// now emit a Decrypted event but keep the decryption failure
pollStartEvent.emit(MatrixEventEvent.Decrypted, pollStartEvent);
// still do not expect a poll to show up for the room
expect(room.polls.get(pollStartEventId)).toBeUndefined();
// clear decryption failure and emit a Decrypted event again
isDecryptionFailureSpy.mockRestore();
pollStartEvent.emit(MatrixEventEvent.Decrypted, pollStartEvent);
// the poll should now show up in the room's polls
const poll = room.polls.get(pollStartEventId);
expect(poll?.pollId).toBe(pollStartEventId);
});
});
@@ -3307,6 +3348,7 @@ describe("Room", function () {
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
isInitialSyncComplete: jest.fn().mockReturnValue(false),
supportsThreads: jest.fn().mockReturnValue(true),
});
});
@@ -3344,11 +3386,16 @@ describe("Room", function () {
newRoomId: string,
predecessorRoomId: string,
tombstoneEventId: string | null = null,
viaServers: string[] = [],
): MatrixEvent {
const content =
tombstoneEventId === null
? { predecessor_room_id: predecessorRoomId }
: { predecessor_room_id: predecessorRoomId, last_known_event_id: tombstoneEventId };
? { predecessor_room_id: predecessorRoomId, via_servers: viaServers }
: {
predecessor_room_id: predecessorRoomId,
last_known_event_id: tombstoneEventId,
via_servers: viaServers,
};
return new MatrixEvent({
content,
@@ -3387,7 +3434,8 @@ describe("Room", function () {
const useMsc3946 = true;
expect(room.findPredecessor(useMsc3946)).toEqual({
roomId: "otherreplacedroomid",
eventId: null, // m.predecessor did not include an event_id
eventId: undefined, // m.predecessor did not include an event_id
viaServers: [],
});
});
@@ -3395,12 +3443,13 @@ describe("Room", function () {
const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([
roomCreateEvent("roomid", "replacedroomid"),
predecessorEvent("roomid", "otherreplacedroomid", "lstevtid"),
predecessorEvent("roomid", "otherreplacedroomid", "lstevtid", ["one.example.com", "two.example.com"]),
]);
const useMsc3946 = true;
expect(room.findPredecessor(useMsc3946)).toEqual({
roomId: "otherreplacedroomid",
eventId: "lstevtid",
viaServers: ["one.example.com", "two.example.com"],
});
});
+1 -1
View File
@@ -53,7 +53,7 @@ describe("RustCrypto", () => {
});
});
describe("to-device messages", () => {
describe("call preprocess methods", () => {
let rustCrypto: RustCrypto;
beforeEach(async () => {
+26 -23
View File
@@ -58,10 +58,12 @@ describe("MatrixScheduler", function () {
let yieldedA = false;
scheduler.setProcessFunction(function (event) {
if (yieldedA) {
// eslint-disable-next-line jest/no-conditional-expect
expect(event).toEqual(eventB);
return deferB.promise;
} else {
yieldedA = true;
// eslint-disable-next-line jest/no-conditional-expect
expect(event).toEqual(eventA);
return deferA.promise;
}
@@ -89,6 +91,7 @@ describe("MatrixScheduler", function () {
scheduler.setProcessFunction(function (ev) {
procCount += 1;
if (procCount === 1) {
// eslint-disable-next-line jest/no-conditional-expect
expect(ev).toEqual(eventA);
return deferred.promise;
} else if (procCount === 2) {
@@ -112,7 +115,7 @@ describe("MatrixScheduler", function () {
expect(procCount).toEqual(2);
});
it("should give up if the retryFn on failure returns -1 and try the next event", async function () {
it("should give up if the retryFn on failure returns -1", async function () {
// Queue A & B.
// Reject A and return -1 on retry.
// Expect B to be tried next and the promise for A to be rejected.
@@ -129,9 +132,11 @@ describe("MatrixScheduler", function () {
scheduler.setProcessFunction(function (ev) {
procCount += 1;
if (procCount === 1) {
// eslint-disable-next-line jest/no-conditional-expect
expect(ev).toEqual(eventA);
return deferA.promise;
} else if (procCount === 2) {
// eslint-disable-next-line jest/no-conditional-expect
expect(ev).toEqual(eventB);
return deferB.promise;
}
@@ -139,22 +144,18 @@ describe("MatrixScheduler", function () {
return new Promise<Record<string, boolean>>(() => {});
});
const globalA = scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
const queuedA = scheduler.queueEvent(eventA);
const queuedB = scheduler.queueEvent(eventB);
await Promise.resolve();
deferA.reject(new Error("Testerror"));
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
// wait just long enough before it does
await Promise.resolve();
await expect(queuedA).rejects.toThrow("Testerror");
await expect(queuedB).rejects.toThrow("Testerror");
expect(procCount).toEqual(1);
deferA.reject({});
try {
await globalA;
} catch (err) {
await Promise.resolve();
expect(procCount).toEqual(2);
}
});
it("should treat each queue separately", function (done) {
it("should treat each queue separately", async () => {
// Queue messages A B C D.
// Bucket A&D into queue_A
// Bucket B&C into queue_B
@@ -179,13 +180,15 @@ describe("MatrixScheduler", function () {
const expectOrder = [eventA.getId(), eventB.getId(), eventD.getId()];
const deferA = defer<Record<string, boolean>>();
scheduler.setProcessFunction(function (event) {
const id = expectOrder.shift();
expect(id).toEqual(event.getId());
if (expectOrder.length === 0) {
done();
}
return id === eventA.getId() ? deferA.promise : deferred.promise;
const allExpectedEventsSeenInOrderPromise = new Promise((resolve) => {
scheduler.setProcessFunction(function (event) {
const id = expectOrder.shift();
expect(id).toEqual(event.getId());
if (expectOrder.length === 0) {
resolve(null);
}
return id === eventA.getId() ? deferA.promise : deferred.promise;
});
});
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
@@ -197,6 +200,7 @@ describe("MatrixScheduler", function () {
deferA.resolve({});
}, 1000);
jest.advanceTimersByTime(1000);
await allExpectedEventsSeenInOrderPromise;
});
describe("queueEvent", function () {
@@ -294,7 +298,7 @@ describe("MatrixScheduler", function () {
});
describe("setProcessFunction", function () {
it("should call the processFn if there are queued events", function () {
it("should call the processFn if there are queued events", async () => {
queueFn = function () {
return "yep";
};
@@ -307,9 +311,8 @@ describe("MatrixScheduler", function () {
});
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
// wait just long enough before it does
Promise.resolve().then(() => {
expect(procCount).toEqual(1);
});
await Promise.resolve();
expect(procCount).toEqual(1);
});
it("should not call the processFn if there are no queued events", function () {
+270
View File
@@ -0,0 +1,270 @@
/*
Copyright 2019, 2022-2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Mocked } from "jest-mock";
import {
AccountDataClient,
PassphraseInfo,
SecretStorageCallbacks,
SecretStorageKeyDescriptionAesV1,
SecretStorageKeyDescriptionCommon,
ServerSideSecretStorageImpl,
trimTrailingEquals,
} from "../../src/secret-storage";
import { calculateKeyCheck } from "../../src/crypto/aes";
import { randomString } from "../../src/randomstring";
describe("ServerSideSecretStorageImpl", function () {
describe(".addKey", function () {
it("should allow storing a default key", async function () {
const accountDataAdapter = mockAccountDataClient();
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const result = await secretStorage.addKey("m.secret_storage.v1.aes-hmac-sha2");
// it should have made up a 32-character key id
expect(result.keyId.length).toEqual(32);
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith(
`m.secret_storage.key.${result.keyId}`,
result.keyInfo,
);
});
it("should allow storing a key with an explicit id", async function () {
const accountDataAdapter = mockAccountDataClient();
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const result = await secretStorage.addKey("m.secret_storage.v1.aes-hmac-sha2", {}, "myKeyId");
// it should have made up a 32-character key id
expect(result.keyId).toEqual("myKeyId");
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith(
"m.secret_storage.key.myKeyId",
result.keyInfo,
);
});
it("should allow storing a key with a name", async function () {
const accountDataAdapter = mockAccountDataClient();
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const result = await secretStorage.addKey("m.secret_storage.v1.aes-hmac-sha2", { name: "mykey" });
expect(result.keyInfo.name).toEqual("mykey");
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith(
`m.secret_storage.key.${result.keyId}`,
result.keyInfo,
);
});
it("should allow storing a key with a passphrase", async function () {
const accountDataAdapter = mockAccountDataClient();
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const passphrase: PassphraseInfo = {
algorithm: "m.pbkdf2",
iterations: 125,
salt: "saltygoodness",
bits: 256,
};
const result = await secretStorage.addKey("m.secret_storage.v1.aes-hmac-sha2", {
passphrase,
});
expect(result.keyInfo.passphrase).toEqual(passphrase);
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith(
`m.secret_storage.key.${result.keyId}`,
result.keyInfo,
);
});
it("should complain about invalid algorithm", async function () {
const accountDataAdapter = mockAccountDataClient();
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
await expect(() => secretStorage.addKey("bad_alg")).rejects.toThrow("Unknown key algorithm");
});
});
describe("getKey", function () {
it("should return the specified key", async function () {
const accountDataAdapter = mockAccountDataClient();
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
if (eventType === "m.secret_storage.key.my_key") {
return storedKey as unknown as T;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
}
accountDataAdapter.getAccountDataFromServer.mockImplementation(mockGetAccountData);
const result = await secretStorage.getKey("my_key");
expect(result).toEqual(["my_key", storedKey]);
});
it("should return the default key if none is specified", async function () {
const accountDataAdapter = mockAccountDataClient();
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
if (eventType === "m.secret_storage.default_key") {
return { key: "default_key_id" } as unknown as T;
} else if (eventType === "m.secret_storage.key.default_key_id") {
return storedKey as unknown as T;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
}
accountDataAdapter.getAccountDataFromServer.mockImplementation(mockGetAccountData);
const result = await secretStorage.getKey();
expect(result).toEqual(["default_key_id", storedKey]);
});
it("should return null if the key is not found", async function () {
const accountDataAdapter = mockAccountDataClient();
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
// @ts-ignore
accountDataAdapter.getAccountDataFromServer.mockResolvedValue(null);
const result = await secretStorage.getKey("my_key");
expect(result).toEqual(null);
});
});
describe("checkKey", function () {
it("should return true for a correct key check", async function () {
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
const myKey = new TextEncoder().encode(randomString(32));
const { iv, mac } = await calculateKeyCheck(myKey);
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
name: "my key",
passphrase: {} as PassphraseInfo,
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
iv,
mac,
};
const result = await secretStorage.checkKey(myKey, keyInfo);
expect(result).toBe(true);
});
it("should return false for an incorrect key check", async function () {
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
const { iv, mac } = await calculateKeyCheck(new TextEncoder().encode("badkey"));
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
name: "my key",
passphrase: {} as PassphraseInfo,
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
iv,
mac,
};
const result = await secretStorage.checkKey(new TextEncoder().encode("goodkey"), keyInfo);
expect(result).toBe(false);
});
it("should raise for an unknown algorithm", async function () {
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
name: "my key",
passphrase: {} as PassphraseInfo,
algorithm: "bad_alg",
iv: "iv",
mac: "mac",
};
await expect(() => secretStorage.checkKey(new TextEncoder().encode("goodkey"), keyInfo)).rejects.toThrow(
"Unknown algorithm",
);
});
// XXX: really???
it("should return true for an absent mac", async function () {
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
name: "my key",
passphrase: {} as PassphraseInfo,
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
iv: "iv",
mac: "",
};
const result = await secretStorage.checkKey(new TextEncoder().encode("goodkey"), keyInfo);
expect(result).toBe(true);
});
});
describe("store", () => {
it("should ignore keys with unknown algorithm", async function () {
const accountDataAdapter = mockAccountDataClient();
const mockCallbacks = { getSecretStorageKey: jest.fn() } as Mocked<SecretStorageCallbacks>;
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, mockCallbacks);
// stub out getAccountData to return a key with an unknown algorithm
const storedKey = { algorithm: "badalg" } as SecretStorageKeyDescriptionCommon;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
if (eventType === "m.secret_storage.key.keyid") {
return storedKey as unknown as T;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
}
accountDataAdapter.getAccountDataFromServer.mockImplementation(mockGetAccountData);
// suppress the expected warning on the console
jest.spyOn(console, "warn").mockImplementation();
// now attempt the store
await secretStorage.store("mysecret", "supersecret", ["keyid"]);
// we should have stored... nothing
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("mysecret", { encrypted: {} });
// ... and emitted a warning.
// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("unknown algorithm"));
});
});
});
describe("trimTrailingEquals", () => {
it("should strip trailing =", () => {
expect(trimTrailingEquals("ab=c===")).toEqual("ab=c");
});
it("should leave strings without trailing = alone", () => {
expect(trimTrailingEquals("ab=c")).toEqual("ab=c");
});
it("should leave the empty string alone", () => {
const result = trimTrailingEquals("");
expect(result).toEqual("");
});
});
function mockAccountDataClient(): Mocked<AccountDataClient> {
return {
getAccountDataFromServer: jest.fn().mockResolvedValue(null),
setAccountData: jest.fn().mockResolvedValue({}),
} as unknown as Mocked<AccountDataClient>;
}
@@ -0,0 +1,47 @@
/*
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 "fake-indexeddb/auto";
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
import { IndexedDBStoreWorker } from "../../../src/store/indexeddb-store-worker";
import { defer } from "../../../src/utils";
function setupWorker(worker: IndexedDBStoreWorker): void {
worker.onMessage({ data: { command: "setupWorker", args: [] } } as any);
worker.onMessage({ data: { command: "connect", seq: 1 } } as any);
}
describe("IndexedDBStore Worker", () => {
it("should pass 'closed' event via postMessage", async () => {
const deferred = defer<void>();
const postMessage = jest.fn().mockImplementation(({ seq, command }) => {
if (seq === 1 && command === "cmd_success") {
deferred.resolve();
}
});
const worker = new IndexedDBStoreWorker(postMessage);
setupWorker(worker);
await deferred.promise;
// @ts-ignore - private field access
(worker.backend as LocalIndexedDBStoreBackend).db!.onclose!({} as Event);
expect(postMessage).toHaveBeenCalledWith({
command: "closed",
});
});
});
+88
View File
@@ -166,4 +166,92 @@ describe("IndexedDBStore", () => {
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
});
it("should emit 'closed' if database is unexpectedly closed", async () => {
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
});
await store.startup();
const deferred = defer<void>();
store.on("closed", deferred.resolve);
// @ts-ignore - private field access
(store.backend as LocalIndexedDBStoreBackend).db!.onclose!({} as Event);
await deferred.promise;
});
it("should use remote backend if workerFactory passed", async () => {
const deferred = defer<void>();
class MockWorker {
postMessage(data: any) {
if (data.command === "setupWorker") {
deferred.resolve();
}
}
}
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
workerFactory: () => new MockWorker() as Worker,
});
store.startup();
await deferred.promise;
});
it("remote worker should pass closed event", async () => {
const worker = new (class MockWorker {
postMessage(data: any) {}
})() as Worker;
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
workerFactory: () => worker,
});
store.startup();
const deferred = defer<void>();
store.on("closed", deferred.resolve);
(worker as any).onmessage({ data: { command: "closed" } });
await deferred.promise;
});
it("remote worker should pass command failures", async () => {
const worker = new (class MockWorker {
private onmessage!: (data: any) => void;
postMessage(data: any) {
if (data.command === "setupWorker" || data.command === "connect") {
this.onmessage({
data: {
command: "cmd_success",
seq: data.seq,
},
});
return;
}
this.onmessage({
data: {
command: "cmd_fail",
seq: data.seq,
error: new Error("Test"),
},
});
}
})() as unknown as Worker;
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
workerFactory: () => worker,
});
await expect(store.startup()).rejects.toThrow("Test");
});
});
+1 -1
View File
@@ -59,7 +59,7 @@ describe("MemoryStore", () => {
await store.deleteAllData();
// empty object
expect(store.accountData).toEqual({});
expect(store.accountData).toEqual(new Map());
});
});
});
+104
View File
@@ -24,9 +24,12 @@ import {
lexicographicCompare,
nextString,
prevString,
recursiveMapToObject,
simpleRetryOperation,
stringToBase,
sortEventsByLatestContentTimestamp,
safeSet,
MapWithDefault,
} from "../../src/utils";
import { logger } from "../../src/logger";
import { mkMessage } from "../test-utils/test-utils";
@@ -145,9 +148,11 @@ describe("utils", function () {
describe("deepCompare", function () {
const assert = {
isTrue: function (x: any) {
// eslint-disable-next-line jest/no-standalone-expect
expect(x).toBe(true);
},
isFalse: function (x: any) {
// eslint-disable-next-line jest/no-standalone-expect
expect(x).toBe(false);
},
};
@@ -604,6 +609,105 @@ describe("utils", function () {
});
});
describe("recursiveMapToObject", () => {
it.each([
// empty map
{
map: new Map(),
expected: {},
},
// one level map
{
map: new Map<any, any>([
["key1", "value 1"],
["key2", 23],
["key3", undefined],
["key4", null],
["key5", [1, 2, 3]],
]),
expected: { key1: "value 1", key2: 23, key3: undefined, key4: null, key5: [1, 2, 3] },
},
// two level map
{
map: new Map<any, any>([
[
"key1",
new Map<any, any>([
["key1_1", "value 1"],
["key1_2", "value 1.2"],
]),
],
["key2", "value 2"],
]),
expected: { key1: { key1_1: "value 1", key1_2: "value 1.2" }, key2: "value 2" },
},
// multi level map
{
map: new Map<any, any>([
["key1", new Map<any, any>([["key1_1", new Map<any, any>([["key1_1_1", "value 1.1.1"]])]])],
]),
expected: { key1: { key1_1: { key1_1_1: "value 1.1.1" } } },
},
// list of maps
{
map: new Map<any, any>([
[
"key1",
[new Map<any, any>([["key1_1", "value 1.1"]]), new Map<any, any>([["key1_2", "value 1.2"]])],
],
]),
expected: { key1: [{ key1_1: "value 1.1" }, { key1_2: "value 1.2" }] },
},
// map → array → array → map
{
map: new Map<any, any>([["key1", [[new Map<any, any>([["key2", "value 2"]])]]]]),
expected: {
key1: [
[
{
key2: "value 2",
},
],
],
},
},
])("%# should convert the value", ({ map, expected }) => {
expect(recursiveMapToObject(map)).toStrictEqual(expected);
});
});
describe("safeSet", () => {
it("should set a value", () => {
const obj: Record<string, string> = {};
safeSet(obj, "testProp", "test value");
expect(obj).toEqual({ testProp: "test value" });
});
it.each(["__proto__", "prototype", "constructor"])("should raise an error when setting »%s«", (prop) => {
expect(() => {
safeSet(<Record<string, string>>{}, prop, "teset value");
}).toThrow("Trying to modify prototype or constructor");
});
});
describe("MapWithDefault", () => {
it("getOrCreate should create the value if it does not exist", () => {
const newValue = {};
const map = new MapWithDefault(() => newValue);
// undefined before getOrCreate
expect(map.get("test")).toBeUndefined();
expect(map.getOrCreate("test")).toBe(newValue);
// default value after getOrCreate
expect(map.get("test")).toBe(newValue);
// test that it always returns the same value
expect(map.getOrCreate("test")).toBe(newValue);
});
});
describe("sleep", () => {
it("resolves", async () => {
await utils.sleep(0);
+85 -2
View File
@@ -431,6 +431,58 @@ describe("Call", function () {
expect(transceivers.get("m.usermedia:video")!.sender.track!.id).toBe("usermedia_video_track");
});
it("should handle error on call upgrade", async () => {
const onError = jest.fn();
call.on(CallEvent.Error, onError);
await startVoiceCall(client, call);
await call.onAnswerReceived(
makeMockEvent("@test:foo", {
version: 1,
call_id: call.callId,
party_id: "party_id",
answer: {
sdp: DUMMY_SDP,
},
[SDPStreamMetadataKey]: {},
}),
);
const mockGetUserMediaStream = jest.fn().mockRejectedValue(new Error("Test error"));
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;
// then unmute which should cause an upgrade
await call.setLocalVideoMuted(false);
expect(onError).toHaveBeenCalled();
});
it("should unmute video after upgrading to video call", async () => {
// Regression test for https://github.com/vector-im/element-call/issues/925
await startVoiceCall(client, call);
// start off with video muted
await call.setLocalVideoMuted(true);
await call.onAnswerReceived(
makeMockEvent("@test:foo", {
version: 1,
call_id: call.callId,
party_id: "party_id",
answer: {
sdp: DUMMY_SDP,
},
[SDPStreamMetadataKey]: {},
}),
);
// then unmute which should cause an upgrade
await call.setLocalVideoMuted(false);
// video should now be unmuted
expect(call.isLocalVideoMuted()).toBe(false);
});
it("should handle SDPStreamMetadata changes", async () => {
await startVoiceCall(client, call);
@@ -712,11 +764,22 @@ describe("Call", function () {
const dataChannel = call.createDataChannel("data_channel_label", { id: 123 });
expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel);
expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel, call);
expect(dataChannel.label).toBe("data_channel_label");
expect(dataChannel.id).toBe(123);
});
it("should emit a data channel event when the other side adds a data channel", async () => {
await startVoiceCall(client, call);
const dataChannelCallback = jest.fn();
call.on(CallEvent.DataChannel, dataChannelCallback);
(call.peerConn as unknown as MockRTCPeerConnection).triggerIncomingDataChannel();
expect(dataChannelCallback).toHaveBeenCalled();
});
describe("supportsMatrixCall", () => {
it("should return true when the environment is right", () => {
expect(supportsMatrixCall()).toBe(true);
@@ -1579,7 +1642,7 @@ describe("Call", function () {
hasAdvancedBy += advanceBy;
expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
expect(lengthChangedListener).toBeCalledWith(hasAdvancedBy);
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy, call);
}
});
@@ -1609,4 +1672,24 @@ describe("Call", function () {
expect(call.hangup).not.toHaveBeenCalled();
});
});
describe("Call replace", () => {
it("Fires event when call replaced", async () => {
const onReplace = jest.fn();
call.on(CallEvent.Replaced, onReplace);
await call.placeVoiceCall();
const call2 = new MatrixCall({
client: client.client,
roomId: FAKE_ROOM_ID,
});
call2.on(CallEvent.Error, errorListener);
await fakeIncomingCall(client, call2);
call.replacedBy(call2);
expect(onReplace).toHaveBeenCalled();
});
});
});
+2 -2
View File
@@ -73,7 +73,7 @@ describe("CallFeed", () => {
});
describe("muting after calling setAudioVideoMuted()", () => {
it("should mute audio by default ", () => {
it("should mute audio by default", () => {
// @ts-ignore Mock
feed.stream.addTrack(new MockMediaStreamTrack("track", "audio", true));
feed.setAudioVideoMuted(true, false);
@@ -102,7 +102,7 @@ describe("CallFeed", () => {
[CallState.Connected, true],
[CallState.Connecting, false],
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
call.emit(CallEvent.State, state);
call.emit(CallEvent.State, state, CallState.InviteSent, call.typed());
expect(feed.connected).toBe(expected);
});
+172 -26
View File
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { EventType, GroupCallIntent, GroupCallType, MatrixCall, MatrixEvent, Room, RoomMember } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall";
@@ -95,16 +97,16 @@ const FAKE_STATE_EVENTS = [
},
];
const mockGetStateEvents = (type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
if (type === EventType.GroupCallMemberPrefix) {
return userId === undefined
? (FAKE_STATE_EVENTS as MatrixEvent[])
: (FAKE_STATE_EVENTS.find((e) => e.getStateKey() === userId) as MatrixEvent);
} else {
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
return userId === undefined ? [fakeEvent] : fakeEvent;
}
};
const mockGetStateEvents =
(events: MatrixEvent[] = FAKE_STATE_EVENTS as MatrixEvent[]) =>
(type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
if (type === EventType.GroupCallMemberPrefix) {
return userId === undefined ? events : events.find((e) => e.getStateKey() === userId) ?? null;
} else {
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
return userId === undefined ? [fakeEvent] : fakeEvent;
}
};
const ONE_HOUR = 1000 * 60 * 60;
@@ -142,15 +144,26 @@ describe("Group Call", function () {
} as unknown as RoomMember;
});
afterEach(() => {
groupCall.leave();
});
it.each(Object.values(GroupCallState).filter((v) => v !== GroupCallState.LocalCallFeedUninitialized))(
"throws when initializing local call feed in %s state",
async (state: GroupCallState) => {
// @ts-ignore
groupCall.state = state;
await expect(groupCall.initLocalCallFeed()).rejects.toThrowError();
await expect(groupCall.initLocalCallFeed()).rejects.toThrow();
},
);
it.each([0, 3, 5, 10, 5000])("sets correct creation timestamp when creating a call", async (time: number) => {
jest.spyOn(Date, "now").mockReturnValue(time);
await groupCall.create();
expect(groupCall.creationTs).toBe(time);
});
it("does not initialize local call feed, if it already is", async () => {
await groupCall.initLocalCallFeed();
jest.spyOn(groupCall, "initLocalCallFeed");
@@ -161,6 +174,25 @@ describe("Group Call", function () {
groupCall.leave();
});
it("does not start initializing local call feed twice", () => {
const promise1 = groupCall.initLocalCallFeed();
// @ts-ignore Mock
groupCall.state = GroupCallState.LocalCallFeedUninitialized;
const promise2 = groupCall.initLocalCallFeed();
expect(promise1).toEqual(promise2);
});
it("sets state to local call feed uninitialized when getUserMedia() fails", async () => {
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValue("Error");
try {
await groupCall.initLocalCallFeed();
} catch (e) {}
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
});
it("stops initializing local call feed when leaving", async () => {
const initPromise = groupCall.initLocalCallFeed();
groupCall.leave();
@@ -317,6 +349,20 @@ describe("Group Call", function () {
}
});
it("does not throw when calling updateLocalUsermediaStream() without local usermedia stream", () => {
expect(async () => await groupCall.updateLocalUsermediaStream({} as MediaStream)).not.toThrow();
});
it.each([GroupCallState.Ended, GroupCallState.Entered, GroupCallState.InitializingLocalCallFeed])(
"throws when entering call in the wrong state",
async (state: GroupCallState) => {
// @ts-ignore Mock
groupCall.state = state;
await expect(groupCall.enter()).rejects.toThrow();
},
);
describe("hasLocalParticipant()", () => {
it("should return false, if we don't have a local participant", () => {
expect(groupCall.hasLocalParticipant()).toBeFalsy();
@@ -349,7 +395,7 @@ describe("Group Call", function () {
jest.spyOn(call, "getOpponentMember").mockReturnValue({ userId: undefined });
// @ts-ignore Mock
expect(() => groupCall.onCallFeedsChanged(call)).toThrowError();
expect(() => groupCall.onCallFeedsChanged(call)).toThrow();
});
describe("usermedia feeds", () => {
@@ -527,7 +573,7 @@ describe("Group Call", function () {
// the call starts muted, so unmute to get in the right state to test
await groupCall.setMicrophoneMuted(false);
mockCall.localUsermediaFeed.setAudioVideoMuted.mockReset();
mocked(mockCall.localUsermediaFeed.setAudioVideoMuted).mockReset();
let metadataUpdateResolve: () => void;
const metadataUpdatePromise = new Promise<void>((resolve) => {
@@ -648,15 +694,15 @@ describe("Group Call", function () {
expect(client1.sendToDevice.mock.calls[0][0]).toBe("m.call.invite");
const toDeviceCallContent = client1.sendToDevice.mock.calls[0][1];
expect(Object.keys(toDeviceCallContent).length).toBe(1);
expect(Object.keys(toDeviceCallContent)[0]).toBe(FAKE_USER_ID_2);
expect(toDeviceCallContent.size).toBe(1);
expect(toDeviceCallContent.has(FAKE_USER_ID_2)).toBe(true);
const toDeviceBobDevices = toDeviceCallContent[FAKE_USER_ID_2];
expect(Object.keys(toDeviceBobDevices).length).toBe(1);
expect(Object.keys(toDeviceBobDevices)[0]).toBe(FAKE_DEVICE_ID_2);
const toDeviceBobDevices = toDeviceCallContent.get(FAKE_USER_ID_2);
expect(toDeviceBobDevices?.size).toBe(1);
expect(toDeviceBobDevices?.has(FAKE_DEVICE_ID_2)).toBe(true);
const bobDeviceMessage = toDeviceBobDevices[FAKE_DEVICE_ID_2];
expect(bobDeviceMessage.conf_id).toBe(FAKE_CONF_ID);
const bobDeviceMessage = toDeviceBobDevices?.get(FAKE_DEVICE_ID_2);
expect(bobDeviceMessage?.conf_id).toBe(FAKE_CONF_ID);
} finally {
await Promise.all([groupCall1.leave(), groupCall2.leave()]);
}
@@ -752,7 +798,7 @@ describe("Group Call", function () {
call.isLocalVideoMuted = jest.fn().mockReturnValue(true);
call.setLocalVideoMuted = jest.fn();
call.emit(CallEvent.State, CallState.Connected);
call.emit(CallEvent.State, CallState.Connected, CallState.InviteSent, call);
expect(call.setMicrophoneMuted).toHaveBeenCalledWith(false);
expect(call.setLocalVideoMuted).toHaveBeenCalledWith(false);
@@ -771,7 +817,7 @@ describe("Group Call", function () {
mockClient = typedMockClient as unknown as MatrixClient;
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents);
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
@@ -835,6 +881,34 @@ describe("Group Call", function () {
groupCall.terminate();
});
it("returns false when unmuting audio with no audio device", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "hasAudioDevice").mockResolvedValue(false);
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
});
it("returns false when no permission for audio stream", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
new Error("No Permission"),
);
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
});
it("returns false when unmuting video with no video device", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "hasVideoDevice").mockResolvedValue(false);
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
});
it("returns false when no permission for video stream", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
new Error("No Permission"),
);
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
});
});
describe("remote muting", () => {
@@ -919,7 +993,14 @@ describe("Group Call", function () {
mockClient = typedMockClient as unknown as MatrixClient;
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2);
room.getMember = jest.fn().mockImplementation((userId) => ({ userId }));
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
} as unknown as RoomMember;
room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
} as unknown as RoomMember;
groupCall = await createAndEnterGroupCall(mockClient, room);
});
@@ -992,6 +1073,71 @@ describe("Group Call", function () {
expect(call.answerWithCallFeeds).toHaveBeenCalled();
});
const aliceEnters = () => {
room.currentState.getStateEvents = jest.fn().mockImplementation(
mockGetStateEvents([
{
getContent: () => ({
"m.calls": [
{
"m.call_id": groupCall.groupCallId,
"m.devices": [
{
device_id: FAKE_DEVICE_ID_1,
session_id: FAKE_SESSION_ID_1,
expires_ts: Date.now() + ONE_HOUR,
feeds: [],
},
],
},
],
}),
getStateKey: () => FAKE_USER_ID_1,
getRoomId: () => FAKE_ROOM_ID,
getTs: () => 0,
},
] as unknown as MatrixEvent[]),
);
room.currentState.emit(RoomStateEvent.Update, room.currentState);
};
const aliceLeaves = () => {
room.currentState.getStateEvents = jest
.fn()
.mockImplementation(mockGetStateEvents([] as unknown as MatrixEvent[]));
room.currentState.emit(RoomStateEvent.Update, room.currentState);
};
it("enables tracks on expected calls, then disables them when the participant leaves", async () => {
aliceEnters();
const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
mockCall.answerWithCallFeeds.mockImplementation(([feed]) => (mockCall.localUsermediaFeed = feed));
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
// Tracks should be enabled
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => t.enabled)).toBe(true);
aliceLeaves();
// Tracks should be disabled
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => !t.enabled)).toBe(true);
});
it("disables tracks on unexpected calls, then enables them when the participant joins", async () => {
const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
mockCall.answerWithCallFeeds.mockImplementation(([feed]) => (mockCall.localUsermediaFeed = feed));
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
// Tracks should be disabled
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => !t.enabled)).toBe(true);
aliceEnters();
// Tracks should be enabled
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => t.enabled)).toBe(true);
});
describe("handles call being replaced", () => {
let callChangedListener: jest.Mock;
let oldMockCall: MockMatrixCall;
@@ -1012,7 +1158,7 @@ describe("Group Call", function () {
});
it("handles regular case", () => {
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed(), oldMockCall.typed());
expect(oldMockCall.hangup).toHaveBeenCalled();
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
@@ -1023,7 +1169,7 @@ describe("Group Call", function () {
it("handles case where call is missing from the calls map", () => {
// @ts-ignore
groupCall.calls = new Map();
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed(), oldMockCall.typed());
expect(oldMockCall.hangup).toHaveBeenCalled();
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
@@ -1089,7 +1235,7 @@ describe("Group Call", function () {
userId: FAKE_USER_ID_2,
membership: "join",
} as unknown as RoomMember;
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents);
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
groupCall = await createAndEnterGroupCall(mockClient, room);
});
+38 -2
View File
@@ -98,6 +98,23 @@ describe("Group Call Event Handler", function () {
expect(groupCall.state).toBe(GroupCallState.Ended);
});
it("terminates call when redacted", async () => {
await groupCallEventHandler.start();
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID), {
roomId: FAKE_ROOM_ID,
} as unknown as RoomState);
const groupCall = groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)!;
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, undefined, true), {
roomId: FAKE_ROOM_ID,
} as unknown as RoomState);
expect(groupCall.state).toBe(GroupCallState.Ended);
});
});
it("waits until client starts syncing", async () => {
@@ -222,9 +239,9 @@ describe("Group Call Event Handler", function () {
jest.clearAllMocks();
});
const setupCallAndStart = async (content?: IContent) => {
const setupCallAndStart = async (content?: IContent, redacted?: boolean) => {
mocked(mockRoom.currentState.getStateEvents).mockReturnValue([
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, content),
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, content, redacted),
] as unknown as MatrixEvent);
mockClient.getRooms.mockReturnValue([mockRoom]);
await groupCallEventHandler.start();
@@ -285,5 +302,24 @@ describe("Group Call Event Handler", function () {
}),
);
});
it("ignores redacted calls", async () => {
await setupCallAndStart(
{
// Real event contents to make sure that it's specifically the
// event being redacted that causes it to be ignored
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
},
true,
);
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
});
});
+12 -2
View File
@@ -48,7 +48,7 @@ describe("Media Handler", function () {
} as unknown as MatrixClient);
});
it("does not trigger update after restore media settings ", () => {
it("does not trigger update after restore media settings", () => {
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
expect(mockMediaDevices.getUserMedia).not.toHaveBeenCalled();
@@ -242,6 +242,11 @@ describe("Media Handler", function () {
);
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
});
it("returns false if the system not permitting access audio inputs", async () => {
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
});
});
describe("hasVideoDevice", () => {
@@ -255,6 +260,11 @@ describe("Media Handler", function () {
);
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
});
it("returns false if the system not permitting access video inputs", async () => {
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
});
});
describe("getUserMediaStream", () => {
@@ -401,7 +411,7 @@ describe("Media Handler", function () {
});
});
describe("stopUserMediaStream", () => {
describe("stopScreensharingStream", () => {
let stream: MediaStream;
beforeEach(async () => {
@@ -0,0 +1,46 @@
/*
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 { ConnectionStatsReporter } from "../../../../src/webrtc/stats/connectionStatsReporter";
describe("ConnectionStatsReporter", () => {
describe("should on bandwidth stats", () => {
it("build bandwidth report if chromium starts attributes available", () => {
const stats = {
availableIncomingBitrate: 1000,
availableOutgoingBitrate: 2000,
} as RTCIceCandidatePairStats;
expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 });
});
it("build empty bandwidth report if chromium starts attributes not available", () => {
const stats = {} as RTCIceCandidatePairStats;
expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 });
});
});
describe("should on connection stats", () => {
it("build bandwidth report if chromium starts attributes available", () => {
const stats = {
availableIncomingBitrate: 1000,
availableOutgoingBitrate: 2000,
} as RTCIceCandidatePairStats;
expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 });
});
it("build empty bandwidth report if chromium starts attributes not available", () => {
const stats = {} as RTCIceCandidatePairStats;
expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 });
});
});
});
@@ -0,0 +1,145 @@
/*
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 { GroupCallStats } from "../../../../src/webrtc/stats/groupCallStats";
import { SummaryStats } from "../../../../src/webrtc/stats/summaryStats";
const GROUP_CALL_ID = "GROUP_ID";
const LOCAL_USER_ID = "LOCAL_USER_ID";
const TIME_INTERVAL = 10000;
describe("GroupCallStats", () => {
let stats: GroupCallStats;
beforeEach(() => {
stats = new GroupCallStats(GROUP_CALL_ID, LOCAL_USER_ID, TIME_INTERVAL);
});
describe("should on adding a stats collector", () => {
it("creating a new one if not existing.", async () => {
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
});
it("creating only one when trying add the same collector multiple times.", async () => {
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeFalsy();
// The User ID is not relevant! Because for stats the call is needed and the user id is for monitoring
expect(stats.addStatsReportGatherer("CALL_ID", "SOME_OTHER_USER_ID", mockRTCPeerConnection())).toBeFalsy();
});
});
describe("should on removing a stats collector", () => {
it("returning `true` if the collector exists", async () => {
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
expect(stats.removeStatsReportGatherer("CALL_ID")).toBeTruthy();
});
it("returning false if the collector not exists", async () => {
expect(stats.removeStatsReportGatherer("CALL_ID_NOT_EXIST")).toBeFalsy();
});
});
describe("should on get stats collector", () => {
it("returning `undefined` if collector not existing", async () => {
expect(stats.getStatsReportGatherer("CALL_ID")).toBeUndefined();
});
it("returning Collector if collector existing", async () => {
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
expect(stats.getStatsReportGatherer("CALL_ID")).toBeDefined();
});
});
describe("should on start", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it("starting processing as well without stats collectors", async () => {
// @ts-ignore
stats.processStats = jest.fn();
stats.start();
jest.advanceTimersByTime(TIME_INTERVAL);
// @ts-ignore
expect(stats.processStats).toHaveBeenCalled();
});
it("starting processing and calling the collectors", async () => {
stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection());
const collector = stats.getStatsReportGatherer("CALL_ID");
stats.reports.emitSummaryStatsReport = jest.fn();
const summaryStats = {
receivedMedia: 0,
receivedAudioMedia: 0,
receivedVideoMedia: 0,
audioTrackSummary: { count: 0, muted: 0 },
videoTrackSummary: { count: 0, muted: 0 },
} as SummaryStats;
let processStatsSpy;
if (collector) {
processStatsSpy = jest.spyOn(collector, "processStats").mockResolvedValue(summaryStats);
stats.start();
jest.advanceTimersByTime(TIME_INTERVAL);
} else {
throw new Error("Test failed, because no Collector found!");
}
expect(processStatsSpy).toHaveBeenCalledWith(GROUP_CALL_ID, LOCAL_USER_ID);
});
it("doing nothing if process already running", async () => {
// @ts-ignore
jest.spyOn(global, "setInterval").mockReturnValue(22);
stats.start();
expect(setInterval).toHaveBeenCalledTimes(1);
stats.start();
stats.start();
stats.start();
stats.start();
expect(setInterval).toHaveBeenCalledTimes(1);
});
});
describe("should on stop", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it("finish stats process if was started", async () => {
// @ts-ignore
jest.spyOn(global, "setInterval").mockReturnValue(22);
jest.spyOn(global, "clearInterval");
stats.start();
expect(setInterval).toHaveBeenCalledTimes(1);
stats.stop();
expect(clearInterval).toHaveBeenCalledWith(22);
});
it("do nothing if stats process was not started", async () => {
jest.spyOn(global, "clearInterval");
stats.stop();
expect(clearInterval).not.toHaveBeenCalled();
});
});
});
const mockRTCPeerConnection = (): RTCPeerConnection => {
const pc = {} as RTCPeerConnection;
pc.addEventListener = jest.fn();
pc.getStats = jest.fn().mockResolvedValue(null);
return pc;
};
@@ -0,0 +1,41 @@
/*
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 { Mid, Ssrc, MediaSsrcHandler } from "../../../../../src/webrtc/stats/media/mediaSsrcHandler";
import { REMOTE_SFU_DESCRIPTION } from "../../../../test-utils/webrtc";
describe("MediaSsrcHandler", () => {
const remoteMap = new Map<Mid, Ssrc[]>([
["0", ["2963372119"]],
["1", ["1212931603"]],
]);
let handler: MediaSsrcHandler;
beforeEach(() => {
handler = new MediaSsrcHandler();
});
describe("should parse description", () => {
it("and build mid ssrc map", async () => {
handler.parse(REMOTE_SFU_DESCRIPTION, "remote");
expect(handler.getSsrcToMidMap("remote")).toEqual(remoteMap);
});
});
describe("should on find mid by ssrc", () => {
it("and return mid if mapping exists.", async () => {
handler.parse(REMOTE_SFU_DESCRIPTION, "remote");
expect(handler.findMidBySsrc("2963372119", "remote")).toEqual("0");
});
});
});
@@ -0,0 +1,127 @@
/*
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 { MediaTrackHandler } from "../../../../../src/webrtc/stats/media/mediaTrackHandler";
describe("TrackHandler", () => {
let pc: RTCPeerConnection;
let handler: MediaTrackHandler;
beforeEach(() => {
pc = {
getTransceivers: (): RTCRtpTransceiver[] => [mockTransceiver("1", "audio"), mockTransceiver("2", "video")],
} as RTCPeerConnection;
handler = new MediaTrackHandler(pc);
});
describe("should get local tracks", () => {
it("returns video track", async () => {
expect(handler.getLocalTracks("video")).toEqual([
{
id: `sender-track-2`,
kind: "video",
} as MediaStreamTrack,
]);
});
it("returns audio track", async () => {
expect(handler.getLocalTracks("audio")).toEqual([
{
id: `sender-track-1`,
kind: "audio",
} as MediaStreamTrack,
]);
});
});
describe("should get local track by mid", () => {
it("returns video track", async () => {
expect(handler.getLocalTrackIdByMid("2")).toEqual("sender-track-2");
});
it("returns audio track", async () => {
expect(handler.getLocalTrackIdByMid("1")).toEqual("sender-track-1");
});
it("returns undefined if not exists", async () => {
expect(handler.getLocalTrackIdByMid("3")).toBeUndefined();
});
});
describe("should get remote track by mid", () => {
it("returns video track", async () => {
expect(handler.getRemoteTrackIdByMid("2")).toEqual("receiver-track-2");
});
it("returns audio track", async () => {
expect(handler.getRemoteTrackIdByMid("1")).toEqual("receiver-track-1");
});
it("returns undefined if not exists", async () => {
expect(handler.getRemoteTrackIdByMid("3")).toBeUndefined();
});
});
describe("should get track by id", () => {
it("returns remote track", async () => {
expect(handler.getTackById("receiver-track-2")).toEqual({
id: `receiver-track-2`,
kind: "video",
} as MediaStreamTrack);
});
it("returns local track", async () => {
expect(handler.getTackById("sender-track-1")).toEqual({
id: `sender-track-1`,
kind: "audio",
} as MediaStreamTrack);
});
it("returns undefined if not exists", async () => {
expect(handler.getTackById("sender-track-3")).toBeUndefined();
});
});
describe("should get simulcast track count", () => {
it("returns 2", async () => {
expect(handler.getActiveSimulcastStreams()).toEqual(3);
});
});
describe("should get Transceiver by Track ID", () => {
it("and returns remote Transceiver if exits", async () => {
expect(handler.getTransceiverByTrackId(`receiver-track-1`)?.mid).toEqual("1");
});
it("and returns local Transceiver if exits", async () => {
expect(handler.getTransceiverByTrackId(`sender-track-2`)?.mid).toEqual("2");
});
it("returns undefined if Transceiver not exits", async () => {
expect(handler.getTransceiverByTrackId("22")).toBeUndefined();
});
});
});
const mockTransceiver = (mid: string, kind: "video" | "audio"): RTCRtpTransceiver => {
return {
mid,
currentDirection: "sendrecv",
sender: {
track: { id: `sender-track-${mid}`, kind } as MediaStreamTrack,
} as RTCRtpSender,
receiver: {
track: { id: `receiver-track-${mid}`, kind } as MediaStreamTrack,
} as RTCRtpReceiver,
} as RTCRtpTransceiver;
};
@@ -0,0 +1,96 @@
/*
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 { MediaTrackHandler } from "../../../../../src/webrtc/stats/media/mediaTrackHandler";
import { MediaTrackStatsHandler } from "../../../../../src/webrtc/stats/media/mediaTrackStatsHandler";
import { MediaSsrcHandler } from "../../../../../src/webrtc/stats/media/mediaSsrcHandler";
describe("MediaTrackStatsHandler", () => {
let statsHandler: MediaTrackStatsHandler;
let ssrcHandler: MediaSsrcHandler;
let trackHandler: MediaTrackHandler;
beforeEach(() => {
ssrcHandler = {} as MediaSsrcHandler;
trackHandler = {} as MediaTrackHandler;
trackHandler.getLocalTrackIdByMid = jest.fn().mockReturnValue("2222");
trackHandler.getRemoteTrackIdByMid = jest.fn().mockReturnValue("5555");
trackHandler.getLocalTracks = jest.fn().mockReturnValue([{ id: "2222" } as MediaStreamTrack]);
trackHandler.getTackById = jest.fn().mockReturnValue([{ id: "2222", kind: "audio" } as MediaStreamTrack]);
statsHandler = new MediaTrackStatsHandler(ssrcHandler, trackHandler);
});
describe("should find track stats", () => {
it("and returns stats if `trackIdentifier` exists in report", () => {
const report = { trackIdentifier: "123" };
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toEqual("123");
});
it("and returns stats if `mid` exists in report", () => {
const reportIn = { mid: "1", type: "inbound-rtp" };
expect(statsHandler.findTrack2Stats(reportIn, "remote")?.trackId).toEqual("5555");
const reportOut = { mid: "1", type: "outbound-rtp" };
expect(statsHandler.findTrack2Stats(reportOut, "local")?.trackId).toEqual("2222");
});
it("and returns undefined if `ssrc` exists in report but not on connection", () => {
const report = { ssrc: "142443", type: "inbound-rtp" };
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue(undefined);
expect(statsHandler.findTrack2Stats(report, "local")?.trackId).toBeUndefined();
});
it("and returns undefined if `ssrc` exists in inbound-rtp report", () => {
const report = { ssrc: "142443", type: "inbound-rtp" };
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue("2");
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toEqual("5555");
});
it("and returns undefined if `ssrc` exists in outbound-rtp report", () => {
const report = { ssrc: "142443", type: "outbound-rtp" };
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue("2");
expect(statsHandler.findTrack2Stats(report, "local")?.trackId).toEqual("2222");
});
it("and returns undefined if needed property not existing", () => {
const report = {};
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toBeUndefined();
});
});
describe("should find local video track stats", () => {
it("and returns stats if `trackIdentifier` exists in report", async () => {
const report = { trackIdentifier: "2222" };
expect(statsHandler.findLocalVideoTrackStats(report)?.trackId).toEqual("2222");
});
it("and returns stats if `mid` exists in report", () => {
const report = { mid: "1" };
expect(statsHandler.findLocalVideoTrackStats(report)?.trackId).toEqual("2222");
});
it("and returns undefined if `ssrc` exists", () => {
const report = { ssrc: "142443", type: "outbound-rtp" };
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue("2");
expect(statsHandler.findTrack2Stats(report, "local")?.trackId).toEqual("2222");
});
it("and returns undefined if needed property not existing", async () => {
const report = {};
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toBeUndefined();
});
});
describe("should find a Transceiver by Track id", () => {
it("and returns undefined if Transceiver not existing", async () => {
trackHandler.getTransceiverByTrackId = jest.fn().mockReturnValue(undefined);
expect(statsHandler.findTransceiverByTrackId("12")).toBeUndefined();
});
it("and returns Transceiver if existing", async () => {
const ts = {} as RTCRtpTransceiver;
trackHandler.getTransceiverByTrackId = jest.fn().mockReturnValue(ts);
expect(statsHandler.findTransceiverByTrackId("12")).toEqual(ts);
});
});
});
@@ -0,0 +1,121 @@
/*
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 { TrackID } from "../../../../src/webrtc/stats/statsReport";
import { MediaTrackStats } from "../../../../src/webrtc/stats/media/mediaTrackStats";
import { StatsReportBuilder } from "../../../../src/webrtc/stats/statsReportBuilder";
describe("StatsReportBuilder", () => {
const LOCAL_VIDEO_TRACK_ID = "LOCAL_VIDEO_TRACK_ID";
const LOCAL_AUDIO_TRACK_ID = "LOCAL_AUDIO_TRACK_ID";
const REMOTE_AUDIO_TRACK_ID = "REMOTE_AUDIO_TRACK_ID";
const REMOTE_VIDEO_TRACK_ID = "REMOTE_VIDEO_TRACK_ID";
const localAudioTrack = new MediaTrackStats(LOCAL_AUDIO_TRACK_ID, "local", "audio");
const localVideoTrack = new MediaTrackStats(LOCAL_VIDEO_TRACK_ID, "local", "video");
const remoteAudioTrack = new MediaTrackStats(REMOTE_AUDIO_TRACK_ID, "remote", "audio");
const remoteVideoTrack = new MediaTrackStats(REMOTE_VIDEO_TRACK_ID, "remote", "video");
const stats = new Map<TrackID, MediaTrackStats>([
[LOCAL_AUDIO_TRACK_ID, localAudioTrack],
[LOCAL_VIDEO_TRACK_ID, localVideoTrack],
[REMOTE_AUDIO_TRACK_ID, remoteAudioTrack],
[REMOTE_VIDEO_TRACK_ID, remoteVideoTrack],
]);
beforeEach(() => {
buildData();
});
describe("should build stats", () => {
it("by media track stats.", async () => {
expect(StatsReportBuilder.build(stats)).toEqual({
bitrate: {
audio: {
download: 4000,
upload: 5000,
},
download: 5004000,
upload: 3005000,
video: {
download: 5000000,
upload: 3000000,
},
},
codec: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", "opus"],
["LOCAL_VIDEO_TRACK_ID", "v8"],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", "opus"],
["REMOTE_VIDEO_TRACK_ID", "v9"],
]),
},
framerate: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", 0],
["LOCAL_VIDEO_TRACK_ID", 30],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", 0],
["REMOTE_VIDEO_TRACK_ID", 60],
]),
},
packetLoss: {
download: 7,
total: 15,
upload: 28,
},
resolution: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }],
["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }],
["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }],
]),
},
jitter: new Map([
["REMOTE_AUDIO_TRACK_ID", 0.1],
["REMOTE_VIDEO_TRACK_ID", 50],
]),
});
});
});
const buildData = (): void => {
localAudioTrack.setCodec("opus");
localAudioTrack.setLoss({ packetsTotal: 10, packetsLost: 5, isDownloadStream: false });
localAudioTrack.setBitrate({ download: 0, upload: 5000 });
remoteAudioTrack.setCodec("opus");
remoteAudioTrack.setLoss({ packetsTotal: 20, packetsLost: 0, isDownloadStream: true });
remoteAudioTrack.setBitrate({ download: 4000, upload: 0 });
remoteAudioTrack.setJitter(0.1);
localVideoTrack.setCodec("v8");
localVideoTrack.setLoss({ packetsTotal: 30, packetsLost: 6, isDownloadStream: false });
localVideoTrack.setBitrate({ download: 0, upload: 3000000 });
localVideoTrack.setFramerate(30);
localVideoTrack.setResolution({ width: 780, height: 460 });
remoteVideoTrack.setCodec("v9");
remoteVideoTrack.setLoss({ packetsTotal: 40, packetsLost: 4, isDownloadStream: true });
remoteVideoTrack.setBitrate({ download: 5000000, upload: 0 });
remoteVideoTrack.setFramerate(60);
remoteVideoTrack.setResolution({ width: 1080, height: 960 });
remoteVideoTrack.setJitter(50);
};
});
@@ -0,0 +1,65 @@
/*
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 { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
import {
ByteSentStatsReport,
ConnectionStatsReport,
StatsReport,
SummaryStatsReport,
} from "../../../../src/webrtc/stats/statsReport";
describe("StatsReportEmitter", () => {
let emitter: StatsReportEmitter;
beforeEach(() => {
emitter = new StatsReportEmitter();
});
it("should emit and receive ByteSendStatsReport", async () => {
const report = {} as ByteSentStatsReport;
return new Promise((resolve, _) => {
emitter.on(StatsReport.BYTE_SENT_STATS, (r) => {
expect(r).toBe(report);
resolve(null);
return;
});
emitter.emitByteSendReport(report);
});
});
it("should emit and receive ConnectionStatsReport", async () => {
const report = {} as ConnectionStatsReport;
return new Promise((resolve, _) => {
emitter.on(StatsReport.CONNECTION_STATS, (r) => {
expect(r).toBe(report);
resolve(null);
return;
});
emitter.emitConnectionStatsReport(report);
});
});
it("should emit and receive SummaryStatsReport", async () => {
const report = {} as SummaryStatsReport;
return new Promise((resolve, _) => {
emitter.on(StatsReport.SUMMARY_STATS, (r) => {
expect(r).toBe(report);
resolve(null);
return;
});
emitter.emitSummaryStatsReport(report);
});
});
});
@@ -0,0 +1,84 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { StatsReportGatherer } from "../../../../src/webrtc/stats/statsReportGatherer";
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
const CALL_ID = "CALL_ID";
const USER_ID = "USER_ID";
describe("StatsReportGatherer", () => {
let collector: StatsReportGatherer;
let rtcSpy: RTCPeerConnection;
let emitter: StatsReportEmitter;
beforeEach(() => {
rtcSpy = { getStats: () => new Promise<RTCStatsReport>(() => null) } as RTCPeerConnection;
rtcSpy.addEventListener = jest.fn();
emitter = new StatsReportEmitter();
collector = new StatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
});
describe("on process stats", () => {
it("if active calculate stats reports", async () => {
const getStats = jest.spyOn(rtcSpy, "getStats");
const report = {} as RTCStatsReport;
report.forEach = jest.fn().mockReturnValue([]);
getStats.mockResolvedValue(report);
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(getStats).toHaveBeenCalled();
expect(actual).toEqual({
receivedMedia: 0,
receivedAudioMedia: 0,
receivedVideoMedia: 0,
audioTrackSummary: { count: 0, muted: 0 },
videoTrackSummary: { count: 0, muted: 0 },
});
expect(collector.getActive()).toBeTruthy();
});
it("if not active do not calculate stats reports", async () => {
collector.setActive(false);
const getStats = jest.spyOn(rtcSpy, "getStats");
await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(getStats).not.toHaveBeenCalled();
});
it("if get reports fails, the collector becomes inactive", async () => {
expect(collector.getActive()).toBeTruthy();
const getStats = jest.spyOn(rtcSpy, "getStats");
getStats.mockRejectedValue(new Error("unknown"));
await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(getStats).toHaveBeenCalled();
expect(collector.getActive()).toBeFalsy();
});
it("if active an RTCStatsReport not a promise the collector becomes inactive", async () => {
const getStats = jest.spyOn(rtcSpy, "getStats");
// @ts-ignore
getStats.mockReturnValue({});
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
expect(actual).toEqual({
receivedMedia: 0,
receivedAudioMedia: 0,
receivedVideoMedia: 0,
audioTrackSummary: { count: 0, muted: 0 },
videoTrackSummary: { count: 0, muted: 0 },
});
expect(getStats).toHaveBeenCalled();
expect(collector.getActive()).toBeFalsy();
});
});
});
@@ -0,0 +1,28 @@
/*
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 { StatsValueFormatter } from "../../../../src/webrtc/stats/statsValueFormatter";
describe("StatsValueFormatter", () => {
describe("on get non negative values", () => {
it("formatter shod return number", async () => {
expect(StatsValueFormatter.getNonNegativeValue("2")).toEqual(2);
expect(StatsValueFormatter.getNonNegativeValue(0)).toEqual(0);
expect(StatsValueFormatter.getNonNegativeValue("-2")).toEqual(0);
expect(StatsValueFormatter.getNonNegativeValue("")).toEqual(0);
expect(StatsValueFormatter.getNonNegativeValue(NaN)).toEqual(0);
});
});
});
@@ -0,0 +1,127 @@
/*
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 { SummaryStatsReporter } from "../../../../src/webrtc/stats/summaryStatsReporter";
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
describe("SummaryStatsReporter", () => {
let reporter: SummaryStatsReporter;
let emitter: StatsReportEmitter;
beforeEach(() => {
emitter = new StatsReportEmitter();
emitter.emitSummaryStatsReport = jest.fn();
reporter = new SummaryStatsReporter(emitter);
});
describe("build Summary Stats Report", () => {
it("should do nothing if summary list empty", async () => {
reporter.build([]);
expect(emitter.emitSummaryStatsReport).not.toHaveBeenCalled();
});
it("should trigger new summary report", async () => {
const summary = [
{
receivedMedia: 10,
receivedAudioMedia: 4,
receivedVideoMedia: 6,
audioTrackSummary: { count: 1, muted: 0 },
videoTrackSummary: { count: 1, muted: 0 },
},
{
receivedMedia: 13,
receivedAudioMedia: 0,
receivedVideoMedia: 13,
audioTrackSummary: { count: 1, muted: 0 },
videoTrackSummary: { count: 1, muted: 0 },
},
{
receivedMedia: 0,
receivedAudioMedia: 0,
receivedVideoMedia: 0,
audioTrackSummary: { count: 1, muted: 0 },
videoTrackSummary: { count: 1, muted: 0 },
},
{
receivedMedia: 15,
receivedAudioMedia: 6,
receivedVideoMedia: 9,
audioTrackSummary: { count: 1, muted: 0 },
videoTrackSummary: { count: 1, muted: 0 },
},
];
reporter.build(summary);
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
percentageReceivedMedia: 0.5,
percentageReceivedAudioMedia: 0.5,
percentageReceivedVideoMedia: 0.75,
});
});
it("as received video Media, although video was not received, but because video muted", async () => {
const summary = [
{
receivedMedia: 10,
receivedAudioMedia: 10,
receivedVideoMedia: 0,
audioTrackSummary: { count: 1, muted: 0 },
videoTrackSummary: { count: 1, muted: 1 },
},
];
reporter.build(summary);
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
percentageReceivedMedia: 1,
percentageReceivedAudioMedia: 1,
percentageReceivedVideoMedia: 1,
});
});
it("as received no video Media, because only on video was muted", async () => {
const summary = [
{
receivedMedia: 10,
receivedAudioMedia: 10,
receivedVideoMedia: 0,
audioTrackSummary: { count: 1, muted: 0 },
videoTrackSummary: { count: 2, muted: 1 },
},
];
reporter.build(summary);
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
percentageReceivedMedia: 0,
percentageReceivedAudioMedia: 1,
percentageReceivedVideoMedia: 0,
});
});
it("as received no audio Media, although audio not received and audio muted", async () => {
const summary = [
{
receivedMedia: 100,
receivedAudioMedia: 0,
receivedVideoMedia: 100,
audioTrackSummary: { count: 1, muted: 1 },
videoTrackSummary: { count: 1, muted: 0 },
},
];
reporter.build(summary);
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
percentageReceivedMedia: 0,
percentageReceivedAudioMedia: 0,
percentageReceivedVideoMedia: 1,
});
});
});
});
@@ -0,0 +1,275 @@
/*
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 { TrackStatsReporter } from "../../../../src/webrtc/stats/trackStatsReporter";
import { MediaTrackStats } from "../../../../src/webrtc/stats/media/mediaTrackStats";
describe("TrackStatsReporter", () => {
describe("should on frame and resolution stats", () => {
it("creating empty frame and resolution report, if no data available.", async () => {
const trackStats = new MediaTrackStats("1", "local", "video");
TrackStatsReporter.buildFramerateResolution(trackStats, {});
expect(trackStats.getFramerate()).toEqual(0);
expect(trackStats.getResolution()).toEqual({ width: -1, height: -1 });
});
it("creating empty frame and resolution report.", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
TrackStatsReporter.buildFramerateResolution(trackStats, {
framesPerSecond: 22.2,
frameHeight: 180,
frameWidth: 360,
});
expect(trackStats.getFramerate()).toEqual(22);
expect(trackStats.getResolution()).toEqual({ width: 360, height: 180 });
});
});
describe("should on simulcast", () => {
it("creating simulcast framerate.", async () => {
const trackStats = new MediaTrackStats("1", "local", "video");
TrackStatsReporter.calculateSimulcastFramerate(
trackStats,
{
framesSent: 100,
timestamp: 1678957001000,
},
{
framesSent: 10,
timestamp: 1678957000000,
},
3,
);
expect(trackStats.getFramerate()).toEqual(30);
});
});
describe("should on bytes received stats", () => {
it("creating build bitrate received report.", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
TrackStatsReporter.buildBitrateReceived(
trackStats,
{
bytesReceived: 2001000,
timestamp: 1678957010,
},
{ bytesReceived: 2000000, timestamp: 1678957000 },
);
expect(trackStats.getBitrate()).toEqual({ download: 800, upload: 0 });
});
});
describe("should on bytes send stats", () => {
it("creating build bitrate send report.", async () => {
const trackStats = new MediaTrackStats("1", "local", "video");
TrackStatsReporter.buildBitrateSend(
trackStats,
{
bytesSent: 2001000,
timestamp: 1678957010,
},
{ bytesSent: 2000000, timestamp: 1678957000 },
);
expect(trackStats.getBitrate()).toEqual({ download: 0, upload: 800 });
});
});
describe("should on codec stats", () => {
it("creating build bitrate send report.", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
const remote = {} as RTCStatsReport;
remote.get = jest.fn().mockReturnValue({ mimeType: "video/v8" });
TrackStatsReporter.buildCodec(remote, trackStats, { codecId: "codecID" });
expect(trackStats.getCodec()).toEqual("v8");
});
});
describe("should on package lost stats", () => {
it("creating build package lost on send report.", async () => {
const trackStats = new MediaTrackStats("1", "local", "video");
TrackStatsReporter.buildPacketsLost(
trackStats,
{
type: "outbound-rtp",
packetsSent: 200,
packetsLost: 120,
},
{
packetsSent: 100,
packetsLost: 30,
},
);
expect(trackStats.getLoss()).toEqual({ packetsTotal: 190, packetsLost: 90, isDownloadStream: false });
});
it("creating build package lost on received report.", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
TrackStatsReporter.buildPacketsLost(
trackStats,
{
type: "inbound-rtp",
packetsReceived: 300,
packetsLost: 100,
},
{
packetsReceived: 100,
packetsLost: 20,
},
);
expect(trackStats.getLoss()).toEqual({ packetsTotal: 280, packetsLost: 80, isDownloadStream: true });
});
});
describe("should set state of a TrackStats", () => {
it("to not alive if Transceiver undefined", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
TrackStatsReporter.setTrackStatsState(trackStats, undefined);
expect(trackStats.alive).toBeFalsy();
});
it("to not alive if Transceiver has no local track", async () => {
const trackStats = new MediaTrackStats("1", "local", "video");
const ts = {
sender: {
track: null,
} as RTCRtpSender,
} as RTCRtpTransceiver;
TrackStatsReporter.setTrackStatsState(trackStats, ts);
expect(trackStats.alive).toBeFalsy();
});
it("to alive if Transceiver remote and track is alive", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
trackStats.alive = false;
const ts = {
receiver: {
track: {
readyState: "live",
enabled: false,
muted: false,
} as MediaStreamTrack,
} as RTCRtpReceiver,
} as RTCRtpTransceiver;
TrackStatsReporter.setTrackStatsState(trackStats, ts);
expect(trackStats.alive).toBeTruthy();
});
it("to alive if Transceiver local and track is live", async () => {
const trackStats = new MediaTrackStats("1", "local", "video");
trackStats.alive = false;
const ts = {
sender: {
track: {
readyState: "live",
enabled: false,
muted: false,
} as MediaStreamTrack,
} as RTCRtpSender,
} as RTCRtpTransceiver;
TrackStatsReporter.setTrackStatsState(trackStats, ts);
expect(trackStats.alive).toBeTruthy();
});
it("to not alive if Transceiver track is ended", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
const ts = {
receiver: {
track: {
readyState: "ended",
enabled: false,
muted: false,
} as MediaStreamTrack,
} as RTCRtpReceiver,
} as RTCRtpTransceiver;
TrackStatsReporter.setTrackStatsState(trackStats, ts);
expect(trackStats.alive).toBeFalsy();
});
it("to not alive and muted if Transceiver track is live and muted", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
const ts = {
receiver: {
track: {
readyState: "live",
enabled: false,
muted: true,
} as MediaStreamTrack,
} as RTCRtpReceiver,
} as RTCRtpTransceiver;
TrackStatsReporter.setTrackStatsState(trackStats, ts);
expect(trackStats.alive).toBeTruthy();
expect(trackStats.muted).toBeTruthy();
});
});
describe("should build Track Summary", () => {
it("and returns empty summary if stats list empty", async () => {
const summary = TrackStatsReporter.buildTrackSummary([]);
expect(summary).toEqual({
audioTrackSummary: {
count: 0,
muted: 0,
},
videoTrackSummary: {
count: 0,
muted: 0,
},
});
});
it("and returns summary if stats list not empty", async () => {
const summary = TrackStatsReporter.buildTrackSummary([]);
expect(summary).toEqual({
audioTrackSummary: {
count: 0,
muted: 0,
},
videoTrackSummary: {
count: 0,
muted: 0,
},
});
});
});
describe("should build jitter value in Track Stats", () => {
it("and returns track stats without jitter if report not 'inbound-rtp'", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
TrackStatsReporter.buildJitter(trackStats, { jitter: 0.01 });
expect(trackStats.getJitter()).toEqual(0);
});
it("and returns track stats with jitter", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
TrackStatsReporter.buildJitter(trackStats, { type: "inbound-rtp", jitter: 0.01 });
expect(trackStats.getJitter()).toEqual(10);
});
it("and returns negative jitter if stats has no jitter value", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
TrackStatsReporter.buildJitter(trackStats, { type: "inbound-rtp" });
expect(trackStats.getJitter()).toEqual(-1);
});
it("and returns jitter as number", async () => {
const trackStats = new MediaTrackStats("1", "remote", "video");
TrackStatsReporter.buildJitter(trackStats, { type: "inbound-rtp", jitter: "0.5" });
expect(trackStats.getJitter()).toEqual(500);
});
});
});
@@ -0,0 +1,126 @@
/*
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 { TransportStatsReporter } from "../../../../src/webrtc/stats/transportStatsReporter";
import { TransportStats } from "../../../../src/webrtc/stats/transportStats";
describe("TransportStatsReporter", () => {
describe("should on build report", () => {
const REMOTE_CANDIDATE_ID = "REMOTE_CANDIDATE_ID";
const LOCAL_CANDIDATE_ID = "LOCAL_CANDIDATE_ID";
const localIC = { ip: "88.88.99.1", port: 56670, protocol: "tcp", candidateType: "local", networkType: "lan" };
const remoteIC = {
ip: "123.88.99.1",
port: 46670,
protocol: "udp",
candidateType: "srfx",
networkType: "wifi",
};
const isFocus = false;
const rtt = 200000;
it("build new transport stats if all properties there", () => {
const { report, stats } = mockStatsReport(isFocus, 0);
const conferenceStatsTransport: TransportStats[] = [];
const transportStats = TransportStatsReporter.buildReport(report, stats, conferenceStatsTransport, isFocus);
expect(transportStats).toEqual([
{
ip: `${remoteIC.ip + 0}:${remoteIC.port}`,
type: remoteIC.protocol,
localIp: `${localIC.ip + 0}:${localIC.port}`,
isFocus,
localCandidateType: localIC.candidateType,
remoteCandidateType: remoteIC.candidateType,
networkType: localIC.networkType,
rtt,
},
]);
});
it("build next transport stats if candidates different", () => {
const mock1 = mockStatsReport(isFocus, 0);
const mock2 = mockStatsReport(isFocus, 1);
let transportStats: TransportStats[] = [];
transportStats = TransportStatsReporter.buildReport(mock1.report, mock1.stats, transportStats, isFocus);
transportStats = TransportStatsReporter.buildReport(mock2.report, mock2.stats, transportStats, isFocus);
expect(transportStats).toEqual([
{
ip: `${remoteIC.ip + 0}:${remoteIC.port}`,
type: remoteIC.protocol,
localIp: `${localIC.ip + 0}:${localIC.port}`,
isFocus,
localCandidateType: localIC.candidateType,
remoteCandidateType: remoteIC.candidateType,
networkType: localIC.networkType,
rtt,
},
{
ip: `${remoteIC.ip + 1}:${remoteIC.port}`,
type: remoteIC.protocol,
localIp: `${localIC.ip + 1}:${localIC.port}`,
isFocus,
localCandidateType: localIC.candidateType,
remoteCandidateType: remoteIC.candidateType,
networkType: localIC.networkType,
rtt,
},
]);
});
it("build not a second transport stats if candidates the same", () => {
const mock1 = mockStatsReport(isFocus, 0);
const mock2 = mockStatsReport(isFocus, 0);
let transportStats: TransportStats[] = [];
transportStats = TransportStatsReporter.buildReport(mock1.report, mock1.stats, transportStats, isFocus);
transportStats = TransportStatsReporter.buildReport(mock2.report, mock2.stats, transportStats, isFocus);
expect(transportStats).toEqual([
{
ip: `${remoteIC.ip + 0}:${remoteIC.port}`,
type: remoteIC.protocol,
localIp: `${localIC.ip + 0}:${localIC.port}`,
isFocus,
localCandidateType: localIC.candidateType,
remoteCandidateType: remoteIC.candidateType,
networkType: localIC.networkType,
rtt,
},
]);
});
const mockStatsReport = (
isFocus: boolean,
prifix: number,
): { report: RTCStatsReport; stats: RTCIceCandidatePairStats } => {
const report = {} as RTCStatsReport;
report.get = (key: string) => {
if (key === LOCAL_CANDIDATE_ID) {
return { ...localIC, ip: localIC.ip + prifix };
}
if (key === REMOTE_CANDIDATE_ID) {
return { ...remoteIC, ip: remoteIC.ip + prifix };
}
// remote
return {};
};
const stats = {
remoteCandidateId: REMOTE_CANDIDATE_ID,
localCandidateId: LOCAL_CANDIDATE_ID,
currentRoundTripTime: 200,
} as RTCIceCandidatePairStats;
return { report, stats };
};
});
});
+1 -1
View File
@@ -20,5 +20,5 @@ export interface IIdentityServerProvider {
* for the associated client.
* @returns Promise which resolves to the access token.
*/
getAccessToken(): Promise<string>;
getAccessToken(): Promise<string | null>;
}
+26
View File
@@ -62,6 +62,8 @@ export function isDmMemberCountCondition(condition: AnyMemberCountCondition): bo
export enum ConditionKind {
EventMatch = "event_match",
EventPropertyIs = "event_property_is",
EventPropertyContains = "event_property_contains",
ContainsDisplayName = "contains_display_name",
RoomMemberCount = "room_member_count",
SenderNotificationPermission = "sender_notification_permission",
@@ -77,9 +79,21 @@ export interface IPushRuleCondition<N extends ConditionKind | string> {
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
key: string;
pattern?: string;
// Note that value property is an optimization for patterns which do not do
// any globbing and when the key is not "content.body".
value?: string;
}
export interface IEventPropertyIsCondition extends IPushRuleCondition<ConditionKind.EventPropertyIs> {
key: string;
value: string | boolean | null | number;
}
export interface IEventPropertyContainsCondition extends IPushRuleCondition<ConditionKind.EventPropertyContains> {
key: string;
value: string | boolean | null | number;
}
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
// no additional fields
}
@@ -105,6 +119,8 @@ export interface ICallStartedPrefixCondition extends IPushRuleCondition<Conditio
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
export type PushRuleCondition =
| IEventMatchCondition
| IEventPropertyIsCondition
| IEventPropertyContainsCondition
| IContainsDisplayNameCondition
| IRoomMemberCountCondition
| ISenderNotificationPermissionCondition
@@ -121,6 +137,8 @@ export enum PushRuleKind {
export enum RuleId {
Master = ".m.rule.master",
IsUserMention = ".org.matrix.msc3952.is_user_mention",
IsRoomMention = ".org.matrix.msc3952.is_room_mention",
ContainsDisplayName = ".m.rule.contains_display_name",
ContainsUserName = ".m.rule.contains_user_name",
AtRoomNotification = ".m.rule.roomnotif",
@@ -133,6 +151,14 @@ export enum RuleId {
IncomingCall = ".m.rule.call",
SuppressNotices = ".m.rule.suppress_notices",
Tombstone = ".m.rule.tombstone",
PollStart = ".m.rule.poll_start",
PollStartUnstable = ".org.matrix.msc3930.rule.poll_start",
PollEnd = ".m.rule.poll_end",
PollEndUnstable = ".org.matrix.msc3930.rule.poll_end",
PollStartOneToOne = ".m.rule.poll_start_one_to_one",
PollStartOneToOneUnstable = ".org.matrix.msc3930.rule.poll_start_one_to_one",
PollEndOneToOne = ".m.rule.poll_end_one_to_one",
PollEndOneToOneUnstable = ".org.matrix.msc3930.rule.poll_end_one_to_one",
}
export type PushRuleSet = {
+6
View File
@@ -112,6 +112,12 @@ export interface LoginTokenPostResponse {
login_token: string;
/**
* Expiration in seconds.
*
* @deprecated this is only provided for compatibility with original revision of the MSC.
*/
expires_in: number;
/**
* Expiration in milliseconds.
*/
expires_in_ms: number;
}
+28 -1
View File
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022-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.
@@ -15,6 +15,7 @@ limitations under the License.
*/
import type { IClearEvent } from "../models/event";
import type { ISignatures } from "./signed";
export type OlmGroupSessionExtraData = {
untrusted?: boolean;
@@ -43,6 +44,10 @@ export interface IEventDecryptionResult {
*/
claimedEd25519Key?: string;
untrusted?: boolean;
/**
* The sender doesn't authorize the unverified devices to decrypt his messages
*/
encryptedDisabledForUnverifiedDevices?: boolean;
}
interface Extensible {
@@ -70,3 +75,25 @@ export interface IMegolmSessionData extends Extensible {
}
/* eslint-enable camelcase */
/** the type of the `device_keys` parameter on `/_matrix/client/v3/keys/upload`
*
* @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
*/
export interface IDeviceKeys {
algorithms: Array<string>;
device_id: string; // eslint-disable-line camelcase
user_id: string; // eslint-disable-line camelcase
keys: Record<string, string>;
signatures?: ISignatures;
}
/** the type of the `one_time_keys` and `fallback_keys` parameters on `/_matrix/client/v3/keys/upload`
*
* @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
*/
export interface IOneTimeKey {
key: string;
fallback?: boolean;
signatures?: ISignatures;
}
+3 -6
View File
@@ -38,7 +38,7 @@ export interface CachedReceipt {
data: Receipt;
}
export type ReceiptCache = { [eventId: string]: CachedReceipt[] };
export type ReceiptCache = Map<string, CachedReceipt[]>;
export interface ReceiptContent {
[eventId: string]: {
@@ -49,11 +49,8 @@ export interface ReceiptContent {
}
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
export type Receipts = {
[receiptType: string]: {
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
};
};
// map: receipt type → user Id → receipt
export type Receipts = Map<string, Map<string, [real: WrappedReceipt | null, synthetic: WrappedReceipt | null]>>;
export type CachedReceiptStructure = {
eventId: string;
+3 -5
View File
@@ -21,6 +21,7 @@ import { MatrixError } from "./http-api";
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
import { MatrixScheduler } from "./scheduler";
import { SyncState } from "./sync";
import { MapWithDefault } from "./utils";
const MAX_BATCH_SIZE = 20;
@@ -122,12 +123,9 @@ export class ToDeviceMessageQueue {
* Attempts to send a batch of to-device messages.
*/
private async sendBatch(batch: IndexedToDeviceBatch): Promise<void> {
const contentMap: Record<string, Record<string, ToDevicePayload>> = {};
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
for (const item of batch.batch) {
if (!contentMap[item.userId]) {
contentMap[item.userId] = {};
}
contentMap[item.userId][item.deviceId] = item.payload;
contentMap.getOrCreate(item.userId).set(item.deviceId, item.payload);
}
logger.info(
+351 -157
View File
File diff suppressed because it is too large Load Diff
+24 -41
View File
@@ -14,31 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
import type { IToDeviceEvent } from "../sync-accumulator";
import type { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import { MatrixEvent } from "../models/event";
import { Room } from "../models/room";
import { CryptoApi } from "../crypto-api";
import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
import { IEncryptedEventInfo } from "../crypto/api";
import { IEventDecryptionResult } from "../@types/crypto";
/**
* Common interface for the crypto implementations
*/
export interface CryptoBackend extends SyncCryptoCallbacks {
/**
* Global override for whether the client should ever send encrypted
* messages to unverified devices. This provides the default for rooms which
* do not specify a value.
*
* If true, all unverified devices will be blacklisted by default
*/
globalBlacklistUnverifiedDevices: boolean;
export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
/**
* Whether sendMessage in a room with unknown and unverified devices
* should throw an error and not send the message. This has 'Global' for
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
* no room-level equivalent for this setting.
*
* @remarks this is here, rather than in `CryptoApi`, because I don't think we're
* going to support it in the rust crypto implementation.
*/
globalErrorOnUnknownDevices: boolean;
@@ -47,16 +42,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
*/
stop(): void;
/**
* Checks if the user has previously published cross-signing keys
*
* This means downloading the devicelist for the user and checking if the list includes
* the cross-signing pseudo-device.
* @returns true if the user has previously published cross-signing keys
*/
userHasCrossSigningKeys(): Promise<boolean>;
/**
* Get the verification level for a given user
*
@@ -76,14 +61,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
*/
checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel;
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param room - the room the event is in
*/
prepareToEncrypt(room: Room): void;
/**
* Encrypt an event according to the configuration of the room.
*
@@ -110,16 +87,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
* @param event - event to be checked
*/
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
/**
* Get a list containing all of the room keys
*
* This should be encrypted before returning it to the user.
*
* @returns a promise which resolves to a list of
* session export objects
*/
exportRoomKeys(): Promise<IMegolmSessionData[]>;
}
/** The methods which crypto implementations should expose to the Sync api */
@@ -138,6 +105,22 @@ export interface SyncCryptoCallbacks {
*/
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
/**
* Called by the /sync loop when one time key counts and unused fallback key details are received.
*
* @param oneTimeKeysCounts - the received one time key counts
* @param unusedFallbackKeys - the received unused fallback keys
*/
processKeyCounts(oneTimeKeysCounts?: Record<string, number>, unusedFallbackKeys?: string[]): Promise<void>;
/**
* Handle the notification from /sync that device lists have
* been changed.
*
* @param deviceLists - device_lists field from /sync
*/
processDeviceLists(deviceLists: IDeviceLists): Promise<void>;
/**
* Called by the /sync loop whenever an m.room.encryption event is received.
*
+3
View File
@@ -202,6 +202,9 @@ export type TopicState = {
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
if (!Array.isArray(mtopic)) {
return { text: content.topic };
}
const text = mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic;
const html = mtopic?.find((r) => r.mimetype === "text/html")?.body;
return { text, html };
+75
View File
@@ -0,0 +1,75 @@
/*
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 { IMegolmSessionData } from "./@types/crypto";
import { Room } from "./models/room";
/**
* Public interface to the cryptography parts of the js-sdk
*
* @remarks Currently, this is a work-in-progress. In time, more methods will be added here.
*/
export interface CryptoApi {
/**
* Global override for whether the client should ever send encrypted
* messages to unverified devices. This provides the default for rooms which
* do not specify a value.
*
* If true, all unverified devices will be blacklisted by default
*/
globalBlacklistUnverifiedDevices: boolean;
/**
* Checks if the user has previously published cross-signing keys
*
* This means downloading the devicelist for the user and checking if the list includes
* the cross-signing pseudo-device.
*
* @returns true if the user has previously published cross-signing keys
*/
userHasCrossSigningKeys(): Promise<boolean>;
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param room - the room the event is in
*/
prepareToEncrypt(room: Room): void;
/**
* Discard any existing megolm session for the given room.
*
* This will ensure that a new session is created on the next call to {@link prepareToEncrypt},
* or the next time a message is sent.
*
* This should not normally be necessary: it should only be used as a debugging tool if there has been a
* problem with encryption.
*
* @param roomId - the room to discard sessions for
*/
forceDiscardSession(roomId: string): Promise<void>;
/**
* Get a list containing all of the room keys
*
* This should be encrypted before returning it to the user.
*
* @returns a promise which resolves to a list of
* session export objects
*/
exportRoomKeys(): Promise<IMegolmSessionData[]>;
}
+8 -6
View File
@@ -25,13 +25,12 @@ import { logger } from "../logger";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
import { decryptAES, encryptAES } from "./aes";
import { DeviceInfo } from "./deviceinfo";
import { SecretStorage } from "./SecretStorage";
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from ".";
import { ISignatures } from "../@types/signed";
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
import { ISecretStorageKeyInfo } from "./api";
import { ServerSideSecretStorage, SecretStorageKeyDescription } from "../secret-storage";
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
@@ -164,12 +163,12 @@ export class CrossSigningInfo {
* key
*/
public async isStoredInSecretStorage(
secretStorage: SecretStorage<MatrixClient | undefined>,
secretStorage: ServerSideSecretStorage,
): Promise<Record<string, object> | null> {
// check what SSSS keys have encrypted the master key (if any)
const stored = (await secretStorage.isStored("m.cross_signing.master")) || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s: Record<string, ISecretStorageKeyInfo>): void {
function intersect(s: Record<string, SecretStorageKeyDescription>): void {
for (const k of Object.keys(stored)) {
if (!s[k]) {
delete stored[k];
@@ -192,7 +191,7 @@ export class CrossSigningInfo {
*/
public static async storeInSecretStorage(
keys: Map<string, Uint8Array>,
secretStorage: SecretStorage<undefined>,
secretStorage: ServerSideSecretStorage,
): Promise<void> {
for (const [type, privateKey] of keys) {
const encodedKey = encodeBase64(privateKey);
@@ -209,7 +208,10 @@ export class CrossSigningInfo {
* @param secretStorage - The secret store using account data
* @returns The private key
*/
public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array | null> {
public static async getFromSecretStorage(
type: string,
secretStorage: ServerSideSecretStorage,
): Promise<Uint8Array | null> {
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
if (!encodedKey) {
return null;

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