Compare commits

...

275 Commits

Author SHA1 Message Date
RiotRobot 33bbd45f1e v25.0.0 2023-04-25 09:42:44 +01:00
RiotRobot fd91a534c7 Prepare changelog for v25.0.0 2023-04-25 09:42:41 +01:00
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
RiotRobot 182534288c v23.3.0 2023-02-14 10:21:24 +00:00
RiotRobot f81b7e5e6f Prepare changelog for v23.3.0 2023-02-14 10:21:21 +00:00
ElementRobot 6dda9e532d Include 'browser' in list of adjusted properties in release.sh (#3149) (#3151)
(cherry picked from commit 5e17626fe0)

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2023-02-14 09:58:17 +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
RiotRobot f61db81961 v23.3.0-rc.1 2023-02-07 12:00:46 +00:00
RiotRobot e2a694115f Prepare changelog for v23.3.0-rc.1 2023-02-07 12:00:44 +00:00
Michael Telatynski 71cf812d24 Add @typescript-eslint/no-base-to-string (#3129) 2023-02-07 10:08:03 +00:00
Richard van der Hoff 8a3d7d5671 Element-R: log outgoing HTTP requests (#3127)
otherwise it's rather hard to see them, at least in Firefox.
2023-02-06 10:50:05 +00:00
Richard van der Hoff 2a363598dd Element-R: fix a bug which prevented encryption working after a reload (#3126)
Upgrade matrix-sdk-crypto-js, to pick up
https://github.com/matrix-org/matrix-rust-sdk/pull/1429
2023-02-03 16:16:32 +00:00
Richard van der Hoff 05bf6428bc Element-R: implement encryption of outgoing events (#3122)
This PR wires up the Rust-SDK into the event encryption path
2023-02-03 15:58:50 +00:00
Michael Telatynski e492a44dde Update typescript bits to aid matrix-react-sdk achieve noImplicitAny (#3079) 2023-02-03 14:01:53 +00:00
Richard van der Hoff 44d2e47f96 Fix invite processing on Element-R (#3121)
Currently, whenever we receive an invite on element R, it crashes the sync
loop. A quick fix to make it not do that.
2023-02-03 10:56:24 +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
Richard van der Hoff 8f5db463e7 Element R: Fix obscure errors when we fail to decrypt to-device events (#3117)
Previously, if we failed to decrypt a to-device event, we would raise an
"expected a string" error when we later tried to decrypt it as a room
event. This at least makes the error clearer.
2023-02-02 08:28:44 +00:00
Kerry 4e8affafcc Poll model - page /relations results (#3073)
* first cut poll model

* process incoming poll relations

* allow alt event types in relations model

* allow alt event types in relations model

* remove unneccesary checks on remove relation

* comment

* Revert "allow alt event types in relations model"

This reverts commit e578d84464403d4a15ee8a7cf3ac643f4fb86d69.

* Revert "Revert "allow alt event types in relations model""

This reverts commit 515db7a8bc2df5a1c619a37c86e17ccbe287ba7a.

* basic handling for new poll relations

* tests

* test room.processPollEvents

* join processBeaconEvents and poll events in client

* tidy and set 23 copyrights

* use rooms instance of matrixClient

* tidy

* more copyright

* simplify processPollEvent code

* throw when poll start event has no roomId

* updates for events-sdk move

* more type changes for events-sdk changes

* page poll relation results

* validate poll end event senders

* reformatted copyright

* undo more comment reformatting

* test paging

* use correct pollstartevent type

* emit after updating _isFetchingResponses state

* make rootEvent public readonly

* fix poll end validation logic to allow poll creator to end poll regardless of redaction
2023-02-01 20:44:40 +00:00
Kerry 2800681bb1 Poll model - validate end events (#3072)
* first cut poll model

* process incoming poll relations

* allow alt event types in relations model

* allow alt event types in relations model

* remove unneccesary checks on remove relation

* comment

* Revert "allow alt event types in relations model"

This reverts commit e578d84464403d4a15ee8a7cf3ac643f4fb86d69.

* Revert "Revert "allow alt event types in relations model""

This reverts commit 515db7a8bc2df5a1c619a37c86e17ccbe287ba7a.

* basic handling for new poll relations

* tests

* test room.processPollEvents

* join processBeaconEvents and poll events in client

* tidy and set 23 copyrights

* use rooms instance of matrixClient

* tidy

* more copyright

* simplify processPollEvent code

* throw when poll start event has no roomId

* updates for events-sdk move

* more type changes for events-sdk changes

* validate poll end event senders

* reformatted copyright

* undo more comment reformatting

* fix poll end validation logic to allow poll creator to end poll regardless of redaction

* Update src/models/poll.ts

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

* correct creator == sender validationin poll end

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-02-01 20:32:37 +00:00
Andy Balaam b2a9e6f12f Handle optional last_known_event_id property in m.predecessor (#3119) 2023-02-01 14:31:07 +00:00
Andy Balaam fbd2c97f87 Move the logic of roomPredecessor into the RoomState class (#3114)
* Move the logic of roomPredecessor into the RoomState class

* Fix review comments
2023-02-01 10:44:32 +00:00
Germain 6c6304a620 Cleanup pre MSC3773 thread unread notif logic (#3115) 2023-01-31 16:59:13 +00:00
Richard van der Hoff 0c1d5f6b25 Element-R: implement remaining OutgoingMessage request types (#3083)
This is a follow-up to #3019: it implements the remaining two types of message types, now that rust SDK has sensibly-shaped types for them.
2023-01-31 15:44:14 +00:00
RiotRobot 1c26dc0233 Resetting package fields for development 2023-01-31 10:47:39 +00:00
RiotRobot a163a202e7 Merge branch 'master' into develop 2023-01-31 10:47:34 +00:00
RiotRobot e15cf9976f v23.2.0 2023-01-31 10:46:15 +00:00
RiotRobot bdc3926417 Prepare changelog for v23.2.0 2023-01-31 10:46:12 +00:00
Hubert Chathi 4f918f684e add support for stable identifier for fixed MAC in SAS verification (#3101) 2023-01-30 09:26:43 -05:00
Germain c142232f4d Stop labelling threads as experimental (#3064) 2023-01-30 11:25:27 +00:00
Šimon Brandner c9bc20aa4d Don't throw with no opponentDeviceInfo (#3107) 2023-01-26 16:28:56 +00:00
renovate[bot] 5c0cb3a536 fix(deps): update all non-major dependencies (#3099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 14:50:36 +00:00
Andy Balaam 415576d0a0 Provide eventId as well as roomId from Room.findPredecessor (#3095) 2023-01-26 10:58:33 +00:00
Andy Balaam 4f9fad66e4 MSC3946 Dynamic room predecessors (#3042)
* Implement MSC3946 for getVisibleRooms

* Implement MSC3946 for getRoomUpgradeHistory
2023-01-26 10:28:07 +00:00
renovate[bot] ebd9854980 chore(deps): update dependency @types/jest to v29.2.6 (#3096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:42:54 +00:00
renovate[bot] cb2fab64d8 chore(deps): update typescript-eslint monorepo to v5.48.2 (#3097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:41:59 +00:00
renovate[bot] f446b49e49 fix(deps): update dependency @babel/runtime to v7.20.13 (#3100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:41:40 +00:00
renovate[bot] 22f5e41058 chore(deps): update dependency @types/uuid to v9 (#3102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:41:33 +00:00
renovate[bot] fee5a006f1 chore(deps): update dependency rimraf to v4 (#3103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:41:21 +00:00
Kerry ef51ee28fd Poll model (#3036)
* first cut poll model

* process incoming poll relations

* allow alt event types in relations model

* allow alt event types in relations model

* remove unneccesary checks on remove relation

* comment

* Revert "allow alt event types in relations model"

This reverts commit e578d84464403d4a15ee8a7cf3ac643f4fb86d69.

* Revert "Revert "allow alt event types in relations model""

This reverts commit 515db7a8bc2df5a1c619a37c86e17ccbe287ba7a.

* basic handling for new poll relations

* tests

* test room.processPollEvents

* join processBeaconEvents and poll events in client

* tidy and set 23 copyrights

* use rooms instance of matrixClient

* tidy

* more copyright

* simplify processPollEvent code

* throw when poll start event has no roomId

* updates for events-sdk move

* more type changes for events-sdk changes

* comment
2023-01-26 02:07:55 +00:00
David Baker cb61345780 Merge pull request #3091 from matrix-org/dbkr/video_mute_no_renegotiate
Remove video tracks on video mute without renegotiating
2023-01-25 15:38:51 +00:00
David Baker a18d4e226e Actually check audio sender too 2023-01-25 15:07:51 +00:00
David Baker b09b33eb4c Add tests 2023-01-25 15:06:36 +00:00
Clark Fischer 5fedc06d7c Remove flaky test (#3098)
I introduced a flaky test to confirm that `MegolmEncryption#prepareToEncrypt`
didn't block the main thread too much, but it turns out that, when run in
varying environments, it tends to fail.

The same behavior is guaranteed by the following cancellation test - if the
thread is blocked, it can't be cancelled.

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-25 14:27:02 +00:00
David Baker d8c9f6db33 Check we have both a sender and a track to send 2023-01-25 11:20:25 +00:00
David Baker 0af5fa0328 Actually check we have a sender, not just a transceiver 2023-01-25 11:16:19 +00:00
David Baker b328b72cd5 Move timeout clear to be with its friends 2023-01-25 11:07:14 +00:00
David Baker ce2a9d7036 Fix test 2023-01-25 10:59:03 +00:00
Andy Balaam 66ae985af5 Refactor getRoomUpgradeHistory to use Room.findPredecessorRoomId (#3090)
* Refactor getRoomUpgradeHistory to use Room.findPredecessorRoomId

* Simplify getRoomUpgradeHistory implementation a little
2023-01-25 09:48:56 +01:00
David Baker 1828e2849c Remove timer on call terminate 2023-01-24 20:30:10 +00:00
David Baker b4f8b0fe4f Remove video tracks on video mute without renegotiating 2023-01-24 18:19:19 +00:00
Robin 70656e954f Merge pull request #3035 from clarkf/megolm-cancellation
Reduce Megolm blocking and add cancellation
2023-01-24 12:26:23 -05:00
Robin 40a4c8d954 Merge branch 'develop' into megolm-cancellation 2023-01-24 12:20:42 -05:00
Andy Balaam 7ed787b86a Fix bug in getRoomUpgradeHistory's verifyLinks functionality (#3089) 2023-01-24 13:44:03 +00:00
RiotRobot 1f58ee7f2c v23.2.0-rc.1 2023-01-24 11:21:25 +00:00
RiotRobot 2e28e9117a Prepare changelog for v23.2.0-rc.1 2023-01-24 11:21:22 +00:00
Andy Balaam a58a36e062 Tests for getRoomUpgradeHistory (#3088) 2023-01-24 10:45:04 +00:00
kegsay 6cf6a0c522 refactor: sliding sync: swap to lists-as-keys (#3086)
* refactor: sliding sync: swap to lists-as-keys

Update the request/response API shape to match the latest
MSC3575 version, which converts `lists` from being an array
of list objects to being a map of list objects.

* Linting

* prettier

* add extra setListRanges test

* Default to right type
2023-01-23 15:26:25 +00:00
Andy Balaam 02aa3edda4 Revert "refactor: sliding sync: swap to lists-as-keys (#3076)"
Reverting because the companion matrix-react-sdk change is not ready so
this is breaking our builds.

This reverts commit e04ea02c62.
2023-01-23 12:21:47 +00:00
kegsay e04ea02c62 refactor: sliding sync: swap to lists-as-keys (#3076)
* refactor: sliding sync: swap to lists-as-keys

Update the request/response API shape to match the latest
MSC3575 version, which converts `lists` from being an array
of list objects to being a map of list objects.

* Linting

* prettier

* add extra setListRanges test

* Default to right type
2023-01-23 11:45:22 +00:00
David Baker 64197bf4db Merge pull request #3082 from matrix-org/dbkr/ptt_null_member_workarounds
Add null check for our own member event
2023-01-20 13:11:16 +00:00
RiotRobot c309fe6942 Merge branch 'master' into develop 2023-01-20 12:28:33 +00:00
RiotRobot 8408f36c12 v23.1.1 2023-01-20 12:26:59 +00:00
RiotRobot dcd8f91e02 Prepare changelog for v23.1.1 2023-01-20 12:26:57 +00:00
ElementRobot cabe14d7e2 replace .at(-1) with array.length-1 (#3080) (#3081)
(cherry picked from commit baeb4acddf)

Co-authored-by: Germain <germains@element.io>
2023-01-20 12:13:36 +00:00
David Baker c019f2bb19 Add null check for our own member event
As per comment
2023-01-20 12:09:28 +00:00
Germain baeb4acddf replace .at(-1) with array.length-1 (#3080) 2023-01-20 12:05:35 +00:00
David Baker 4a6e9a0f8f Merge pull request #3078 from matrix-org/dbkr/group_call_double_stream_init
Handle group call getting initialised twice in quick succession
2023-01-20 11:01:46 +00:00
David Baker ea5ce8d1d8 Also wrap the initLocalFeed method of groupCall 2023-01-19 15:28:56 +00:00
David Baker 41854918a5 Merge branch 'develop' into dbkr/group_call_double_stream_init 2023-01-19 14:13:35 +00:00
David Baker 495642d041 Handle group call getting initialised twice in quick succession
This happens in dev mode with React 18 and caused extra user media
stream to end up floating around.

Fixes https://github.com/vector-im/element-call/issues/847
2023-01-19 14:04:59 +00:00
Richard van der Hoff c7210b9e9d Rename some of the .spec files which test crypto (#3077)
* `matrix-client-crypto.spec.ts` only tested a very specific bit of crypto (olm
  encryption). It goes back to the very early days, before Megolm was invented.
  I've renamed it to `olm-encryption-spec.ts`.

* `megolm-integ.spec.ts` is more of a general crypto test; it was just called
  `megolm` to distinguish it from the Olm tests above. Renamed to
  `crypto.spec.ts`.
2023-01-19 12:20:21 +00:00
Richard van der Hoff 83563c7a01 Implement decryption via the rust sdk (#3074)
A bunch of changes to tests, and wire up decryption.
2023-01-18 16:47:44 +00:00
RiotRobot 2fcc4811dd Resetting package fields for development 2023-01-18 13:38:53 +00:00
RiotRobot c392bc455d Merge branch 'master' into develop 2023-01-18 13:36:45 +00:00
RiotRobot b8711f15fd v23.1.0 2023-01-18 13:30:21 +00:00
RiotRobot 81f3aef960 Prepare changelog for v23.1.0 2023-01-18 13:30:19 +00:00
Richard van der Hoff d6b8332567 Element-R: stub implementations of some methods (#3075)
These are all called by the react-sdk when showing an encrypted event:

 * `getEventEncryptionInfo`
 * `checkUserTrust`
 * `checkDeviceTrust`

I don't particularly want to keep this API, but as a rapid means to an end,
let's stub them for now.
2023-01-18 12:07:49 +00:00
Richard van der Hoff 85b34b46c5 Remove brokenheaded encryption test (#3070)
This test seemed to be testing the behaviour of decrypting redacted events, but
that seems... strange. A redaction event cannot be encrypted (at least, there
is no spec for it), and it should be impossible to decrypt a (correctly)
redacted event, because such an event will lack a `ciphertext` property.

This test is just sticking a "redacted_because" property into a regular event,
which is a bit of a nonsense.
2023-01-17 11:27:30 +00:00
RiotRobot 4179f2978d v23.1.0-rc.4 2023-01-17 09:11:54 +00:00
RiotRobot 97df6db49c Prepare changelog for v23.1.0-rc.4 2023-01-17 09:11:52 +00:00
ElementRobot 3e693fab23 [Backport staging] Correctly handle limited sync responses by resetting the thread timeline (#3069)
Co-authored-by: Janne Mareike Koschinski <jannemk@element.io>
2023-01-17 09:06:28 +00:00
Clark Fischer 1ee487a2ff Make prepareToEncrypt cancellable.
NOTE: This commit introduces a backwards-compatible API change.

Adds the ability to cancel `MegolmEncryption#prepareToEncrypt` by returning
a cancellation function. The bulk of the processing happens in
`getDevicesInRoom`, which now accepts a 'getter' that allows the caller to
indicate cancellation.

See https://github.com/matrix-org/matrix-js-sdk/issues/1255
Closes #1255

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-16 10:23:13 -08:00
Clark Fischer b76e7ca782 Reduce blocking while pre-fetching Megolm keys
Currently, calling `Client#prepareToEncrypt` in a megolm room has the potential
to block for multiple seconds while it crunches numbers.

Sleeping for 0 seconds (approximating `setImmediate`) allows the engine to
process other events, updates, or re-renders in between checks.

See
- https://github.com/vector-im/element-web/issues/21612
- https://github.com/vector-im/element-web/issues/11836

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-16 10:11:37 -08:00
Clark Fischer ddce1bcd28 Add async setImmediate util
Adds an async/promise-based version of `setImmediate`. Note that, despite being
poorly adopted, `setImmediate` is polyfilled, and should be more performant
than `sleep(0)`.

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-16 10:11:37 -08:00
Janne Mareike Koschinski a34d06c7c2 Correctly handle limited sync responses by resetting the thread timeline (#3056)
* Reset thread livetimelines when desynced
* Implement workaround for https://github.com/matrix-org/synapse/issues/14830
2023-01-16 16:27:28 +00:00
David Baker 7b10fa367d Merge pull request #3066 from matrix-org/dbkr/olm_savesession_undecryptable_todevice_debug
Add some debugging & a debug event for decryption
2023-01-13 21:40:36 +00:00
Šimon Brandner 7f5d7091de Fix typos in src/webrtc/ (#3065) 2023-01-13 19:56:55 +01:00
David Baker 96f673ae92 Merge branch 'develop' into dbkr/olm_savesession_undecryptable_todevice_debug 2023-01-13 18:36:22 +00:00
David Baker 79faee7a67 Add emit so tests don't throw 2023-01-13 18:32:21 +00:00
David Baker 89d2984432 Add some debugging & a debug event for decryption
Adds a log line whenever we save a session and also adds an event
that's fired whenever we get a to-device event we can't decrypt
(hopefully the comment explains all).
2023-01-13 18:24:33 +00:00
Travis Ralston bc78784688 Extract v1 extensible events polls types out of the events-sdk (#3062)
* Extract v1 extensible events polls out of events-sdk

* Appease tsdoc?

* Appease naming standards

* Bring the tests over too
2023-01-13 10:02:27 -07:00
Richard van der Hoff eb058edb1b Fix spurious "Decryption key withheld" messages (#3061)
When we receive an `m.unavailable` notification, do not show it as "Decryption
key withheld".
2023-01-13 13:17:48 +00:00
Richard van der Hoff 4847d78b42 Improvements to megolm integration tests (#3060)
The megolm tests were making a few assumptions which they really shouldn't; in
particular:

 * They were creating mock events with event_ids not starting `$`, and lacking
   `sender`, `origin_server_ts` and `unsigned` properties

 * They were not including the (now) required `keys.ed25519` property inside
   the ciphertext of an olm message.

These work ok currently, but they aren't really correct, and they cause
problems when testing the new rust implementation.
2023-01-13 13:14:44 +00:00
RiotRobot 94f1eda830 v23.1.0-rc.3 2023-01-13 10:40:31 +00:00
RiotRobot bc2a182ee9 Prepare changelog for v23.1.0-rc.3 2023-01-13 10:40:29 +00:00
renovate[bot] 789aec732a Update typescript-eslint monorepo to v5.48.1 (#3063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-13 09:08:54 +00:00
renovate[bot] 3246114772 Update dependency docdash to v2.0.1 (#3057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-12 18:38:54 +00:00
ElementRobot 39cf1863f1 Fix failure to start in firefox private browser (#3058) (#3059)
(cherry picked from commit aa1e118f18)

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-01-12 17:10:42 +00:00
Richard van der Hoff aa1e118f18 Fix failure to start in firefox private browser (#3058) 2023-01-12 17:03:16 +00:00
Richard van der Hoff d23c3cb8b2 Improve logging in legacy megolm code (#3043)
* Use a PrefixedLogger throughout `megolm.ts`

Rather than manually adding `in ${this.roomId}` to each log line, use a
PrefixedLogger to achieve the same effect more consistently.

* Clean up logging in megolm.ts

Where we log a list of devices, we don't need the whole deviceinfo, just the
device id. All that noise makes it very hard to read the logs.

* Log users that we find in the room when encrypting

* Reduce log verbosity on decryption retries
2023-01-12 11:49:32 +00:00
Richard van der Hoff 9e37980e2d Handle edits which are bundled with an event, per MSC3925 (#3045) 2023-01-12 10:53:36 +00:00
renovate[bot] de176dbd66 Update all non-major dependencies (#3053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-11 16:55:07 +00:00
renovate[bot] ac10b40f67 Update dependency @babel/core to v7.20.12 (#3054)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-11 16:46:47 +00:00
Michael Telatynski d7bf0f85c0 Fix browser entrypoint (#3051) 2023-01-11 15:17:55 +00:00
215 changed files with 19061 additions and 6244 deletions
+1 -1
View File
@@ -23,4 +23,4 @@ indent_size = 4
trim_trailing_whitespace = true
[*.{yml,yaml}]
indent_size = 2
indent_size = 4
+18 -1
View File
@@ -1,6 +1,9 @@
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"],
},
env: {
browser: true,
node: true,
@@ -60,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@e6e25ac3a2b93187502a8be1ef9e9603afc34925 # v2.24.2
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/
+118 -5
View File
@@ -1,11 +1,121 @@
Changes in [23.1.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.2) (2023-01-12)
============================================================================================================
Changes in [25.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.0.0) (2023-04-25)
==================================================================================================
## 🚨 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)
==================================================================================================
## ✨ Features
* Element-R: implement encryption of outgoing events ([\#3122](https://github.com/matrix-org/matrix-js-sdk/pull/3122)).
* Poll model - page /relations results ([\#3073](https://github.com/matrix-org/matrix-js-sdk/pull/3073)). Contributed by @kerryarchibald.
* Poll model - validate end events ([\#3072](https://github.com/matrix-org/matrix-js-sdk/pull/3072)). Contributed by @kerryarchibald.
* Handle optional last_known_event_id property in m.predecessor ([\#3119](https://github.com/matrix-org/matrix-js-sdk/pull/3119)). Contributed by @andybalaam.
* Add support for stable identifier for fixed MAC in SAS verification ([\#3101](https://github.com/matrix-org/matrix-js-sdk/pull/3101)).
* Provide eventId as well as roomId from Room.findPredecessor ([\#3095](https://github.com/matrix-org/matrix-js-sdk/pull/3095)). Contributed by @andybalaam.
* MSC3946 Dynamic room predecessors ([\#3042](https://github.com/matrix-org/matrix-js-sdk/pull/3042)). Contributed by @andybalaam.
* Poll model ([\#3036](https://github.com/matrix-org/matrix-js-sdk/pull/3036)). Contributed by @kerryarchibald.
* Remove video tracks on video mute without renegotiating ([\#3091](https://github.com/matrix-org/matrix-js-sdk/pull/3091)).
* Introduces a backwards-compatible API change. `MegolmEncrypter#prepareToEncrypt`'s return type has changed from `void` to `() => void`. ([\#3035](https://github.com/matrix-org/matrix-js-sdk/pull/3035)). Contributed by @clarkf.
## 🐛 Bug Fixes
* 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.
* Element-R: fix a bug which prevented encryption working after a reload ([\#3126](https://github.com/matrix-org/matrix-js-sdk/pull/3126)).
* Element-R: Fix invite processing ([\#3121](https://github.com/matrix-org/matrix-js-sdk/pull/3121)).
* Don't throw with no `opponentDeviceInfo` ([\#3107](https://github.com/matrix-org/matrix-js-sdk/pull/3107)).
* Remove flaky megolm test ([\#3098](https://github.com/matrix-org/matrix-js-sdk/pull/3098)). Contributed by @clarkf.
* Fix "verifyLinks" functionality of getRoomUpgradeHistory ([\#3089](https://github.com/matrix-org/matrix-js-sdk/pull/3089)). Contributed by @andybalaam.
Changes in [23.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.2.0) (2023-01-31)
==================================================================================================
## ✨ Features
* Implement decryption via the rust sdk ([\#3074](https://github.com/matrix-org/matrix-js-sdk/pull/3074)).
* Handle edits which are bundled with an event, per MSC3925 ([\#3045](https://github.com/matrix-org/matrix-js-sdk/pull/3045)).
## 🐛 Bug Fixes
* Add null check for our own member event ([\#3082](https://github.com/matrix-org/matrix-js-sdk/pull/3082)).
* Handle group call getting initialised twice in quick succession ([\#3078](https://github.com/matrix-org/matrix-js-sdk/pull/3078)). Fixes vector-im/element-call#847.
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952. Contributed by @justjanne.
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
* Fix spurious "Decryption key withheld" messages ([\#3061](https://github.com/matrix-org/matrix-js-sdk/pull/3061)). Fixes vector-im/element-web#23803.
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
Changes in [23.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.1) (2023-01-11)
============================================================================================================
Changes in [23.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.1) (2023-01-20)
==================================================================================================
## 🐛 Bug Fixes
* Fix backwards compability for environment not support Array.prototype.at ([\#3080](https://github.com/matrix-org/matrix-js-sdk/pull/3080)).
Changes in [23.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0) (2023-01-18)
==================================================================================================
## 🦖 Deprecations
* Remove extensible events v1 field population on legacy events ([\#3040](https://github.com/matrix-org/matrix-js-sdk/pull/3040)).
@@ -27,10 +137,13 @@ Changes in [23.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
* Avoid triggering decryption errors when decrypting redacted events ([\#3004](https://github.com/matrix-org/matrix-js-sdk/pull/3004)). Fixes vector-im/element-web#24084.
* bugfix: upload OTKs in sliding sync mode ([\#3008](https://github.com/matrix-org/matrix-js-sdk/pull/3008)).
* Apply edits discovered from sync after thread is initialised ([\#3002](https://github.com/matrix-org/matrix-js-sdk/pull/3002)). Fixes vector-im/element-web#23921.
* sliding sync: Fix issue where no unsubs are sent when switching rooms ([\#2991](https://github.com/matrix-org/matrix-js-sdk/pull/2991)).
* Sliding sync: Fix issue where no unsubs are sent when switching rooms ([\#2991](https://github.com/matrix-org/matrix-js-sdk/pull/2991)).
* Threads are missing from the timeline ([\#2996](https://github.com/matrix-org/matrix-js-sdk/pull/2996)). Fixes vector-im/element-web#24036.
* Close all streams when a call ends ([\#2992](https://github.com/matrix-org/matrix-js-sdk/pull/2992)). Fixes vector-im/element-call#742.
* Resume to-device message queue after resumed sync ([\#2920](https://github.com/matrix-org/matrix-js-sdk/pull/2920)). Fixes matrix-org/element-web-rageshakes#17170.
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952.
Changes in [23.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.0.0) (2022-12-21)
==================================================================================================
+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;
+39 -29
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "23.1.0-rc.2",
"version": "25.0.0",
"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.2",
"@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,11 +84,12 @@
"@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",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "7",
"@types/uuid": "9",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"allchange": "^1.0.6",
@@ -96,53 +97,62 @@
"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.29.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.9.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",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"matrix-mock-request": "^2.5.0",
"prettier": "2.8.1",
"rimraf": "^3.0.2",
"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"
}
+1 -1
View File
@@ -11,7 +11,7 @@ jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
for i in main typings
for i in main typings browser
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
+3 -3
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}"
@@ -180,7 +180,7 @@ yarn version --no-git-tag-version --new-version "$release"
# they exist). This small bit of gymnastics allows us to use the TypeScript
# source directly for development without needing to build before linting or
# testing.
for i in main typings
for i in main typings browser
do
lib_value=$(jq -r ".matrix_lib_$i" package.json)
if [ "$lib_value" != "null" ]; then
@@ -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
+37 -5
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;
@@ -115,13 +124,12 @@ export class TestClient {
}
/**
* Set up expectations that the client will upload device keys.
* Set up expectations that the client will upload device keys (and possibly one-time keys)
*/
public expectDeviceKeyUpload() {
this.httpBackend
.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
logger.log(this + ": received device keys");
@@ -129,7 +137,17 @@ export class TestClient {
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
this.deviceKeys = content.device_keys;
return { one_time_key_counts: { signed_curve25519: 0 } };
// the first batch of one-time keys may be uploaded at the same time.
if (content.one_time_keys) {
logger.log(`${this}: received ${Object.keys(content.one_time_keys).length} one-time keys`);
this.oneTimeKeys = content.one_time_keys;
}
return {
one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
},
};
});
}
@@ -231,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`);
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 () {
+14 -14
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.
@@ -590,7 +590,7 @@ describe("MatrixClient event timelines", function () {
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
@@ -647,7 +647,7 @@ describe("MatrixClient event timelines", function () {
it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
@@ -680,7 +680,7 @@ describe("MatrixClient event timelines", function () {
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
@@ -709,7 +709,7 @@ describe("MatrixClient event timelines", function () {
it("should return undefined when event is within a thread but timelineSet is not", () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
@@ -1127,7 +1127,7 @@ describe("MatrixClient event timelines", function () {
};
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
@@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () {
describe("with server compatibility", function () {
beforeEach(() => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
@@ -1421,7 +1421,7 @@ describe("MatrixClient event timelines", function () {
};
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
@@ -1473,7 +1473,7 @@ describe("MatrixClient event timelines", function () {
describe("without server compatibility", function () {
beforeEach(() => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideListSupport(FeatureSupport.None);
});
@@ -1539,7 +1539,7 @@ describe("MatrixClient event timelines", function () {
it("should add lazy loading filter", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideListSupport(FeatureSupport.Stable);
// @ts-ignore
@@ -1567,7 +1567,7 @@ describe("MatrixClient event timelines", function () {
it("should correctly pass pagination token", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideListSupport(FeatureSupport.Stable);
@@ -1892,7 +1892,7 @@ describe("MatrixClient event timelines", function () {
it("in stable mode", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
@@ -1902,7 +1902,7 @@ describe("MatrixClient event timelines", function () {
it("in backwards compatible unstable mode", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideListSupport(FeatureSupport.Experimental);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Experimental);
@@ -1912,7 +1912,7 @@ describe("MatrixClient event timelines", function () {
it("in backwards compatible mode", async () => {
// @ts-ignore
client.clientOpts.experimentalThreadSupport = true;
client.clientOpts.threadSupport = true;
Thread.setServerSideSupport(FeatureSupport.Experimental);
Thread.setServerSideListSupport(FeatureSupport.None);
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
+67 -45
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.
@@ -35,7 +35,7 @@ describe("MatrixClient", function () {
let store: MemoryStore | undefined;
const defaultClientOpts: IStoredClientOpts = {
experimentalThreadSupport: false,
threadSupport: false,
};
const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => {
const store = new MemoryStore();
@@ -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"],
@@ -671,7 +657,7 @@ describe("MatrixClient", function () {
// @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
threadSupport: true,
};
const eventPollResponseReference = buildEventPollResponseReference();
@@ -702,7 +688,7 @@ describe("MatrixClient", function () {
// @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
threadSupport: true,
};
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
@@ -726,7 +712,7 @@ describe("MatrixClient", function () {
// @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
threadSupport: true,
};
const eventPollResponseReference = buildEventPollResponseReference();
@@ -750,7 +736,7 @@ describe("MatrixClient", function () {
// @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
threadSupport: true,
};
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
@@ -774,7 +760,7 @@ describe("MatrixClient", function () {
// @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
threadSupport: true,
};
// This is based on recording the events in a real room:
@@ -831,7 +817,7 @@ describe("MatrixClient", function () {
// @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
threadSupport: true,
};
const threadRootEvent = buildEventPollStartThreadRoot();
@@ -857,7 +843,7 @@ describe("MatrixClient", function () {
// @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
threadSupport: true,
};
const threadRootEvent = buildEventPollStartThreadRoot();
@@ -878,7 +864,7 @@ describe("MatrixClient", function () {
// @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
threadSupport: true,
};
const threadRootEvent = buildEventPollStartThreadRoot();
@@ -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]: {},
},
};
}
});
File diff suppressed because it is too large Load Diff
@@ -22,20 +22,20 @@ limitations under the License.
*
* Note that megolm (group) conversation is not tested here.
*
* See also `megolm.spec.js`.
* See also `crypto.spec.js`.
*/
// load olm before the sdk if possible
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();
+21 -35
View File
@@ -52,10 +52,9 @@ describe("SlidingSyncSdk", () => {
const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getList = jest.fn();
s.getListParams = jest.fn();
s.getListData = jest.fn();
s.getRoomSubscriptions = jest.fn();
s.listLength = jest.fn();
s.modifyRoomSubscriptionInfo = jest.fn();
s.modifyRoomSubscriptions = jest.fn();
s.registerExtension = jest.fn();
@@ -115,7 +114,7 @@ describe("SlidingSyncSdk", () => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto();
@@ -154,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();
});
});
@@ -549,7 +548,7 @@ describe("SlidingSyncSdk", () => {
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
pos: "h",
lists: [],
lists: {},
rooms: {},
extensions: {},
});
@@ -577,7 +576,7 @@ describe("SlidingSyncSdk", () => {
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
pos: "i",
lists: [],
lists: {},
rooms: {},
extensions: {},
});
@@ -585,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,
@@ -596,7 +595,7 @@ describe("SlidingSyncSdk", () => {
}),
);
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
expect(mockSlidingSync!.stop).toBeCalled();
expect(mockSlidingSync!.stop).toHaveBeenCalled();
});
});
@@ -663,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",
]);
});
});
@@ -892,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",
+151 -147
View File
@@ -64,10 +64,10 @@ describe("SlidingSync", () => {
let slidingSync: SlidingSync;
it("should start the sync loop upon calling start()", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
const fakeResp = {
pos: "a",
lists: [],
lists: {},
rooms: {},
extensions: {},
};
@@ -90,7 +90,7 @@ describe("SlidingSync", () => {
it("should reset the connection on HTTP 400 and send everything again", async () => {
// seed the connection with some lists, extensions and subscriptions to verify they are sent again
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
const roomId = "!sub:localhost";
const subInfo = {
timeline_limit: 42,
@@ -114,7 +114,7 @@ describe("SlidingSync", () => {
};
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
slidingSync.modifyRoomSubscriptionInfo(subInfo);
slidingSync.setList(0, listInfo);
slidingSync.setList("a", listInfo);
slidingSync.registerExtension(ext);
slidingSync.start();
@@ -128,7 +128,7 @@ describe("SlidingSync", () => {
expect(body.room_subscriptions).toEqual({
[roomId]: subInfo,
});
expect(body.lists[0]).toEqual(listInfo);
expect(body.lists["a"]).toEqual(listInfo);
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams!["pos"]).toBeUndefined();
@@ -137,7 +137,7 @@ describe("SlidingSync", () => {
.respond(200, function () {
return {
pos: "11",
lists: [{ count: 5 }],
lists: { a: { count: 5 } },
extensions: {},
txn_id: txnId,
};
@@ -151,7 +151,7 @@ describe("SlidingSync", () => {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({
expect(body.lists["a"]).toEqual({
ranges: [[0, 10]],
});
expect(body.extensions).toBeTruthy();
@@ -161,7 +161,7 @@ describe("SlidingSync", () => {
.respond(200, function () {
return {
pos: "12",
lists: [{ count: 5 }],
lists: { a: { count: 5 } },
extensions: {},
};
});
@@ -185,7 +185,7 @@ describe("SlidingSync", () => {
expect(body.room_subscriptions).toEqual({
[roomId]: subInfo,
});
expect(body.lists[0]).toEqual(listInfo);
expect(body.lists["a"]).toEqual(listInfo);
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams!["pos"]).toBeUndefined();
@@ -193,7 +193,7 @@ describe("SlidingSync", () => {
.respond(200, function () {
return {
pos: "1",
lists: [{ count: 6 }],
lists: { a: { count: 6 } },
extensions: {},
};
});
@@ -221,7 +221,7 @@ describe("SlidingSync", () => {
it("should be able to subscribe to a room", async () => {
// add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
httpBackend!
.when("POST", syncUrl)
@@ -233,7 +233,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "a",
lists: [],
lists: {},
extensions: {},
rooms: {
[roomId]: wantRoomData,
@@ -266,7 +266,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "a",
lists: [],
lists: {},
extensions: {},
rooms: {
[roomId]: wantRoomData,
@@ -313,7 +313,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {
[anotherRoomID]: anotherRoomData,
@@ -344,7 +344,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
});
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
@@ -402,19 +402,19 @@ describe("SlidingSync", () => {
is_dm: true,
},
};
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1);
slidingSync = new SlidingSync(proxyBaseUrl, new Map([["a", listReq]]), {}, client!, 1);
httpBackend!
.when("POST", syncUrl)
.check(function (req) {
const body = req.data;
logger.log("list", body);
expect(body.lists).toBeTruthy();
expect(body.lists[0]).toEqual(listReq);
expect(body.lists["a"]).toEqual(listReq);
})
.respond(200, {
pos: "a",
lists: [
{
lists: {
a: {
count: 500,
ops: [
{
@@ -424,7 +424,7 @@ describe("SlidingSync", () => {
},
],
},
],
},
rooms: rooms,
});
const listenerData: Record<string, MSC3575RoomData> = {};
@@ -444,15 +444,14 @@ describe("SlidingSync", () => {
expect(listenerData[roomB]).toEqual(rooms[roomB]);
expect(listenerData[roomC]).toEqual(rooms[roomC]);
expect(slidingSync.listLength()).toEqual(1);
slidingSync.off(SlidingSyncEvent.RoomData, dataListener);
});
it("should be possible to retrieve list data", () => {
expect(slidingSync.getList(0)).toBeDefined();
expect(slidingSync.getList(5)).toBeNull();
expect(slidingSync.getListData(5)).toBeNull();
const syncData = slidingSync.getListData(0)!;
expect(slidingSync.getListParams("a")).toBeDefined();
expect(slidingSync.getListParams("b")).toBeNull();
expect(slidingSync.getListData("b")).toBeNull();
const syncData = slidingSync.getListData("a")!;
expect(syncData.joinedCount).toEqual(500); // from previous test
expect(syncData.roomIndexToRoomId).toEqual({
0: roomA,
@@ -467,17 +466,17 @@ describe("SlidingSync", () => {
.when("POST", syncUrl)
.check(function (req) {
const body = req.data;
logger.log("next ranges", body.lists[0].ranges);
logger.log("next ranges", body.lists["a"].ranges);
expect(body.lists).toBeTruthy();
expect(body.lists[0]).toEqual({
expect(body.lists["a"]).toEqual({
// only the ranges should be sent as the rest are unchanged and sticky
ranges: newRanges,
});
})
.respond(200, {
pos: "b",
lists: [
{
lists: {
a: {
count: 500,
ops: [
{
@@ -487,15 +486,17 @@ describe("SlidingSync", () => {
},
],
},
],
},
});
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.RequestFinished;
});
slidingSync.setListRanges(0, newRanges);
slidingSync.setListRanges("a", newRanges);
await httpBackend!.flushAllExpected();
await responseProcessed;
// setListRanges for an invalid list key returns an error
await expect(slidingSync.setListRanges("idontexist", newRanges)).rejects.toBeTruthy();
});
it("should be possible to add an extra list", async () => {
@@ -513,19 +514,19 @@ describe("SlidingSync", () => {
const body = req.data;
logger.log("extra list", body);
expect(body.lists).toBeTruthy();
expect(body.lists[0]).toEqual({
expect(body.lists["a"]).toEqual({
// only the ranges should be sent as the rest are unchanged and sticky
ranges: newRanges,
});
expect(body.lists[1]).toEqual(extraListReq);
expect(body.lists["b"]).toEqual(extraListReq);
})
.respond(200, {
pos: "c",
lists: [
{
lists: {
a: {
count: 500,
},
{
b: {
count: 50,
ops: [
{
@@ -535,10 +536,10 @@ describe("SlidingSync", () => {
},
],
},
],
},
});
listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(1);
listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("b");
expect(joinedCount).toEqual(50);
expect(roomIndexToRoomId).toEqual({
0: roomA,
@@ -550,7 +551,7 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
slidingSync.setList(1, extraListReq);
slidingSync.setList("b", extraListReq);
await httpBackend!.flushAllExpected();
await responseProcessed;
});
@@ -559,8 +560,8 @@ describe("SlidingSync", () => {
// move C (2) to A (0)
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e",
lists: [
{
lists: {
a: {
count: 500,
ops: [
{
@@ -574,16 +575,16 @@ describe("SlidingSync", () => {
},
],
},
{
b: {
count: 50,
},
],
},
});
let listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomC,
@@ -603,8 +604,8 @@ describe("SlidingSync", () => {
// move C (0) back to A (2)
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f",
lists: [
{
lists: {
a: {
count: 500,
ops: [
{
@@ -618,13 +619,13 @@ describe("SlidingSync", () => {
},
],
},
{
b: {
count: 50,
},
],
},
});
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomA,
@@ -644,8 +645,8 @@ describe("SlidingSync", () => {
it("should ignore invalid list indexes", async () => {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e",
lists: [
{
lists: {
a: {
count: 500,
ops: [
{
@@ -654,16 +655,16 @@ describe("SlidingSync", () => {
},
],
},
{
b: {
count: 50,
},
],
},
});
const listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomA,
@@ -684,8 +685,8 @@ describe("SlidingSync", () => {
it("should be possible to update a list", async () => {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g",
lists: [
{
lists: {
a: {
count: 42,
ops: [
{
@@ -699,13 +700,13 @@ describe("SlidingSync", () => {
},
],
},
{
b: {
count: 50,
},
],
},
});
// update the list with a new filter
slidingSync.setList(0, {
slidingSync.setList("a", {
filters: {
is_encrypted: true,
},
@@ -714,8 +715,8 @@ describe("SlidingSync", () => {
const listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(42);
expect(roomIndexToRoomId).toEqual({
0: roomB,
@@ -738,12 +739,12 @@ describe("SlidingSync", () => {
0: roomB,
1: roomC,
};
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual(indexToRoomId);
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it
lists: [
{
lists: {
a: {
count: 500,
ops: [
{
@@ -761,16 +762,16 @@ describe("SlidingSync", () => {
},
],
},
{
b: {
count: 50,
},
],
},
});
const listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual(indexToRoomId);
return true;
@@ -785,14 +786,14 @@ describe("SlidingSync", () => {
});
it("should handle deletions correctly", async () => {
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomB,
1: roomC,
});
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g",
lists: [
{
lists: {
a: {
count: 499,
ops: [
{
@@ -801,16 +802,16 @@ describe("SlidingSync", () => {
},
],
},
{
b: {
count: 50,
},
],
},
});
const listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(499);
expect(roomIndexToRoomId).toEqual({
0: roomC,
@@ -827,13 +828,13 @@ describe("SlidingSync", () => {
});
it("should handle insertions correctly", async () => {
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomC,
});
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [
{
lists: {
a: {
count: 500,
ops: [
{
@@ -843,16 +844,16 @@ describe("SlidingSync", () => {
},
],
},
{
b: {
count: 50,
},
],
},
});
let listPromise = listenUntil(
slidingSync,
"SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
(listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomC,
@@ -870,8 +871,8 @@ describe("SlidingSync", () => {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [
{
lists: {
a: {
count: 501,
ops: [
{
@@ -881,13 +882,13 @@ describe("SlidingSync", () => {
},
],
},
{
b: {
count: 50,
},
],
},
});
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
expect(listKey).toEqual("a");
expect(joinedCount).toEqual(501);
expect(roomIndexToRoomId).toEqual({
0: roomC,
@@ -910,11 +911,14 @@ describe("SlidingSync", () => {
it("should handle insertions with a spurious DELETE correctly", async () => {
slidingSync = new SlidingSync(
proxyBaseUrl,
[
{
ranges: [[0, 20]],
},
],
new Map([
[
"a",
{
ranges: [[0, 20]],
},
],
]),
{},
client!,
1,
@@ -922,22 +926,22 @@ describe("SlidingSync", () => {
// initially start with nothing
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "a",
lists: [
{
lists: {
a: {
count: 0,
ops: [],
},
],
},
});
slidingSync.start();
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({});
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({});
// insert a room
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "b",
lists: [
{
lists: {
a: {
count: 1,
ops: [
{
@@ -951,18 +955,18 @@ describe("SlidingSync", () => {
},
],
},
],
},
});
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomA,
});
// insert another room
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "c",
lists: [
{
lists: {
a: {
count: 1,
ops: [
{
@@ -976,10 +980,10 @@ describe("SlidingSync", () => {
},
],
},
],
},
});
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomB,
1: roomA,
});
@@ -987,8 +991,8 @@ describe("SlidingSync", () => {
// insert a final room
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "c",
lists: [
{
lists: {
a: {
count: 1,
ops: [
{
@@ -1002,10 +1006,10 @@ describe("SlidingSync", () => {
},
],
},
],
},
});
await httpBackend!.flushAllExpected();
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
0: roomC,
1: roomB,
2: roomA,
@@ -1028,7 +1032,7 @@ describe("SlidingSync", () => {
required_state: [["m.room.name", ""]],
};
// add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
// modification before SlidingSync.start()
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
let txnId: string | undefined;
@@ -1046,7 +1050,7 @@ describe("SlidingSync", () => {
return {
pos: "aaa",
txn_id: txnId,
lists: [],
lists: {},
extensions: {},
rooms: {
[roomId]: {
@@ -1065,7 +1069,7 @@ describe("SlidingSync", () => {
const newList = {
ranges: [[0, 20]],
};
const promise = slidingSync.setList(0, newList);
const promise = slidingSync.setList("a", newList);
let txnId: string | undefined;
httpBackend!
.when("POST", syncUrl)
@@ -1073,7 +1077,7 @@ describe("SlidingSync", () => {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual(newList);
expect(body.lists["a"]).toEqual(newList);
expect(body.txn_id).toBeTruthy();
txnId = body.txn_id;
})
@@ -1081,7 +1085,7 @@ describe("SlidingSync", () => {
return {
pos: "bbb",
txn_id: txnId,
lists: [{ count: 5 }],
lists: { a: { count: 5 } },
extensions: {},
};
});
@@ -1090,7 +1094,7 @@ describe("SlidingSync", () => {
expect(txnId).toBeDefined();
});
it("should resolve setListRanges during a connection", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]);
const promise = slidingSync.setListRanges("a", [[20, 40]]);
let txnId: string | undefined;
httpBackend!
.when("POST", syncUrl)
@@ -1098,7 +1102,7 @@ describe("SlidingSync", () => {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({
expect(body.lists["a"]).toEqual({
ranges: [[20, 40]],
});
expect(body.txn_id).toBeTruthy();
@@ -1108,7 +1112,7 @@ describe("SlidingSync", () => {
return {
pos: "ccc",
txn_id: txnId,
lists: [{ count: 5 }],
lists: { a: { count: 5 } },
extensions: {},
};
});
@@ -1150,10 +1154,10 @@ describe("SlidingSync", () => {
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data.txn_id);
};
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
const failPromise = slidingSync.setListRanges("a", [[20, 40]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
await httpBackend!.flushAllExpected();
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
const failPromise2 = slidingSync.setListRanges("a", [[60, 70]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
await httpBackend!.flushAllExpected();
@@ -1162,7 +1166,7 @@ describe("SlidingSync", () => {
expect(failPromise).rejects.toEqual(gotTxnIds[0]);
expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
const okPromise = slidingSync.setListRanges("a", [[0, 20]]);
let txnId: string | undefined;
httpBackend!
.when("POST", syncUrl)
@@ -1187,10 +1191,10 @@ describe("SlidingSync", () => {
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
gotTxnIds.push(req.data?.txn_id);
};
const A = slidingSync.setListRanges(0, [[20, 40]]);
const A = slidingSync.setListRanges("a", [[20, 40]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
await httpBackend!.flushAllExpected();
const B = slidingSync.setListRanges(0, [[60, 70]]);
const B = slidingSync.setListRanges("a", [[60, 70]]);
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
await httpBackend!.flushAllExpected();
@@ -1198,7 +1202,7 @@ describe("SlidingSync", () => {
// which is a fail.
expect(A).rejects.toEqual(gotTxnIds[0]);
const C = slidingSync.setListRanges(0, [[0, 20]]);
const C = slidingSync.setListRanges("a", [[0, 20]]);
let pendingC = true;
C.finally(() => {
pendingC = false;
@@ -1219,7 +1223,7 @@ describe("SlidingSync", () => {
expect(pendingC).toBe(true); // C is pending still
});
it("should do nothing for unknown txn_ids", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]);
const promise = slidingSync.setListRanges("a", [[20, 40]]);
let pending = true;
promise.finally(() => {
pending = false;
@@ -1231,7 +1235,7 @@ describe("SlidingSync", () => {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({
expect(body.lists["a"]).toEqual({
ranges: [[20, 40]],
});
expect(body.txn_id).toBeTruthy();
@@ -1241,7 +1245,7 @@ describe("SlidingSync", () => {
return {
pos: "ccc",
txn_id: "bogus transaction id",
lists: [{ count: 5 }],
lists: { a: { count: 5 } },
extensions: {},
};
});
@@ -1279,7 +1283,7 @@ describe("SlidingSync", () => {
};
it("should be possible to use custom subscriptions on startup", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
// the intention is for clients to set this up at startup
slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName2, customSub2);
@@ -1302,7 +1306,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1312,7 +1316,7 @@ describe("SlidingSync", () => {
});
it("should be possible to use custom subscriptions mid-connection", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
// the intention is for clients to set this up at startup
slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName2, customSub2);
@@ -1326,7 +1330,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1344,7 +1348,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1363,7 +1367,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1383,7 +1387,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1395,7 +1399,7 @@ describe("SlidingSync", () => {
});
it("uses the default subscription for unknown subscription names", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.useCustomSubscription(roomA, "unknown name");
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
@@ -1410,7 +1414,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1420,7 +1424,7 @@ describe("SlidingSync", () => {
});
it("should not be possible to add/modify an already added custom subscription", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName1, customSub2);
slidingSync.useCustomSubscription(roomA, customSubName1);
@@ -1436,7 +1440,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1446,7 +1450,7 @@ describe("SlidingSync", () => {
});
it("should change the custom subscription if they are different", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName2, customSub2);
slidingSync.useCustomSubscription(roomA, customSubName1);
@@ -1463,7 +1467,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1484,7 +1488,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1506,7 +1510,7 @@ describe("SlidingSync", () => {
})
.respond(200, {
pos: "b",
lists: [],
lists: {},
extensions: {},
rooms: {},
});
@@ -1559,7 +1563,7 @@ describe("SlidingSync", () => {
};
it("should be able to register an extension", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
slidingSync.registerExtension(extPre);
const callbackOrder: string[] = [];
@@ -1684,7 +1688,7 @@ describe("SlidingSync", () => {
});
it("is not possible to register the same extension name twice", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
slidingSync.registerExtension(extPre);
expect(() => {
slidingSync.registerExtension(extPre);
+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`);
}
}
}
+1
View File
@@ -60,6 +60,7 @@ export const getMockClientWithEventEmitter = (
*/
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
getUserId: jest.fn().mockReturnValue(userId),
getSafeUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
+16 -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);
});
});
@@ -403,3 +403,15 @@ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
pushkey: "pushpush",
...extra,
});
/**
* a list of the supported crypto implementations, each with a callback to initialise that implementation
* for the given client
*/
export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
export type InitCrypto = (_: MatrixClient) => Promise<void>;
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
if (global.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
}
+151 -8
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.
@@ -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();
}
@@ -476,7 +484,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();
public supportsExperimentalThreads(): boolean {
public supportsThreads(): boolean {
return true;
}
public async decryptEventIfNeeded(): Promise<void> {}
@@ -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;
});
});
+1 -2
View File
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
import { M_TOPIC } from "../../src/@types/topic";
import {
@@ -25,6 +23,7 @@ import {
parseBeaconContent,
parseTopicContent,
} from "../../src/content-helpers";
import { REFERENCE_RELATION } from "../../src/@types/extensible_events";
describe("Beacon content helpers", () => {
describe("makeBeaconInfoContent()", () => {
+42 -29
View File
@@ -254,6 +254,7 @@ describe("Crypto", function () {
sendToDevice: jest.fn(),
getKeyBackupVersion: jest.fn(),
isGuest: jest.fn(),
emit: jest.fn(),
} as unknown as MatrixClient;
mockRoomList = {} as unknown as RoomList;
@@ -303,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();
});
@@ -400,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;
@@ -549,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
@@ -563,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",
@@ -613,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
@@ -622,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;
@@ -641,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) => {
@@ -650,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,
@@ -754,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",
};
@@ -808,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 () {
@@ -997,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);
@@ -1025,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));
+123 -31
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.
@@ -16,6 +16,7 @@ limitations under the License.
import { mocked, MockedObject } from "jest-mock";
import type { DeviceInfoMap } from "../../../../src/crypto/DeviceList";
import "../../../olm-loader";
import type { OutboundGroupSession } from "@matrix-org/olm";
import * as algorithms from "../../../../src/crypto/algorithms";
@@ -33,6 +34,8 @@ 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")!;
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
@@ -58,6 +61,12 @@ describe("MegolmDecryption", function () {
beforeEach(async function () {
mockCrypto = testUtils.mock(Crypto, "Crypto") as MockedObject<Crypto>;
// @ts-ignore assigning to readonly prop
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
mockBaseApis = {
claimOneTimeKeys: jest.fn(),
sendToDevice: jest.fn(),
@@ -175,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(() => {
@@ -203,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];
@@ -314,10 +331,6 @@ describe("MegolmDecryption", function () {
let olmDevice: OlmDevice;
beforeEach(async () => {
// @ts-ignore assigning to readonly prop
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const cryptoStore = new MemoryCryptoStore();
olmDevice = new OlmDevice(cryptoStore);
@@ -353,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({
@@ -387,8 +396,10 @@ describe("MegolmDecryption", function () {
mockCrypto.baseApis = mockBaseApis;
mockRoom = {
roomId: ROOM_ID,
getEncryptionTargetMembers: jest.fn().mockReturnValue([{ userId: "@alice:home.server" }]),
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
shouldEncryptForInvitedMembers: jest.fn().mockReturnValue(false),
} as unknown as Room;
});
@@ -513,6 +524,87 @@ describe("MegolmDecryption", function () {
});
});
describe("prepareToEncrypt", () => {
let megolm: MegolmEncryptionClass;
let room: jest.Mocked<Room>;
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>;
room.getEncryptionTargetMembers.mockImplementation(async () => [
new RoomMember(room.roomId, "@user:example.org"),
]);
room.getBlacklistUnverifiedDevices.mockReturnValue(false);
mockCrypto.downloadKeys.mockImplementation(async () => deviceMap);
mockCrypto.checkDeviceTrust.mockImplementation(() => new DeviceTrustLevel(true, true, true, true));
const olmDevice = new OlmDevice(new MemoryCryptoStore());
megolm = new MegolmEncryptionClass({
userId: "@user:id",
deviceId: "12345",
crypto: mockCrypto,
olmDevice,
baseApis: mockBaseApis,
roomId: room.roomId,
config: {
algorithm: "m.megolm.v1.aes-sha2",
rotation_period_ms: 9_999_999,
},
});
});
it("checks each device", async () => {
megolm.prepareToEncrypt(room);
//@ts-ignore private member access, gross
await megolm.encryptionPreparation?.promise;
for (const [userId, devices] of deviceMap) {
for (const deviceId of devices.keys()) {
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledWith(userId, deviceId);
}
}
});
it("is cancellable", async () => {
const stop = megolm.prepareToEncrypt(room);
const before = mockCrypto.checkDeviceTrust.mock.calls.length;
stop();
// Ensure that no more devices were checked after cancellation.
await sleep(10);
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledTimes(before);
});
});
it("notifies devices that have been blocked", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client;
@@ -580,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",
@@ -761,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",
+23 -12
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);
};
@@ -215,7 +220,7 @@ describe("SAS verification", function () {
]);
// make sure that it uses the preferred method
expect(macMethod).toBe("org.matrix.msc3783.hkdf-hmac-sha256");
expect(macMethod).toBe("hkdf-hmac-sha256.v2");
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
// make sure Alice and Bob verified each other
@@ -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 () => {
+6 -6
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.
@@ -147,7 +147,7 @@ describe("EventTimelineSet", () => {
let thread: Thread;
beforeEach(() => {
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
(client.supportsThreads as jest.Mock).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
@@ -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", () => {
@@ -206,7 +206,7 @@ describe("EventTimelineSet", () => {
});
it("should allow edits to be added to thread timeline", async () => {
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
Thread.hasServerSideSupport = FeatureSupport.Stable;
@@ -393,13 +393,13 @@ describe("EventTimelineSet", () => {
let thread: Thread;
beforeEach(() => {
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
(client.supportsThreads as jest.Mock).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
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", () => {
@@ -0,0 +1,41 @@
/*
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.
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 { ExtensibleEventType, IPartialEvent } from "../../../src/@types/extensible_events";
import { ExtensibleEvent } from "../../../src/extensible_events_v1/ExtensibleEvent";
class MockEvent extends ExtensibleEvent<any> {
public constructor(wireEvent: IPartialEvent<any>) {
super(wireEvent);
}
public serialize(): IPartialEvent<object> {
throw new Error("Not implemented for tests");
}
public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
throw new Error("Not implemented for tests");
}
}
describe("ExtensibleEvent", () => {
it("should expose the wire event directly", () => {
const input: IPartialEvent<any> = { type: "org.example.custom", content: { hello: "world" } };
const event = new MockEvent(input);
expect(event.wireFormat).toBe(input);
expect(event.wireContent).toBe(input.content);
});
});
@@ -0,0 +1,156 @@
/*
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.
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 {
ExtensibleAnyMessageEventContent,
IPartialEvent,
M_HTML,
M_MESSAGE,
M_TEXT,
} from "../../../src/@types/extensible_events";
import { MessageEvent } from "../../../src/extensible_events_v1/MessageEvent";
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
describe("MessageEvent", () => {
it("should parse m.text", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_TEXT.name]: "Text here",
},
};
const message = new MessageEvent(input);
expect(message.text).toBe("Text here");
expect(message.html).toBeFalsy();
expect(message.renderings.length).toBe(1);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
});
it("should parse m.html", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_TEXT.name]: "Text here",
[M_HTML.name]: "HTML here",
},
};
const message = new MessageEvent(input);
expect(message.text).toBe("Text here");
expect(message.html).toBe("HTML here");
expect(message.renderings.length).toBe(2);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
});
it("should parse m.message", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_MESSAGE.name]: [
{ body: "Text here", mimetype: "text/plain" },
{ body: "HTML here", mimetype: "text/html" },
{ body: "MD here", mimetype: "text/markdown" },
],
// These should be ignored
[M_TEXT.name]: "WRONG Text here",
[M_HTML.name]: "WRONG HTML here",
},
};
const message = new MessageEvent(input);
expect(message.text).toBe("Text here");
expect(message.html).toBe("HTML here");
expect(message.renderings.length).toBe(3);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
expect(message.renderings.some((r) => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true);
});
it("should fail to parse missing text", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
hello: "world",
} as any, // force invalid type
};
expect(() => new MessageEvent(input)).toThrow(
new InvalidEventError("Missing textual representation for event"),
);
});
it("should fail to parse missing plain text in m.message", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_MESSAGE.name]: [{ body: "HTML here", mimetype: "text/html" }],
},
};
expect(() => new MessageEvent(input)).toThrow(
new InvalidEventError("m.message is missing a plain text representation"),
);
});
it("should fail to parse non-array m.message", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_MESSAGE.name]: "invalid",
} as any, // force invalid type
};
expect(() => new MessageEvent(input)).toThrow(new InvalidEventError("m.message contents must be an array"));
});
describe("from & serialize", () => {
it("should serialize to a legacy fallback", () => {
const message = MessageEvent.from("Text here", "HTML here");
expect(message.text).toBe("Text here");
expect(message.html).toBe("HTML here");
expect(message.renderings.length).toBe(2);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
const serialized = message.serialize();
expect(serialized.type).toBe("m.room.message");
expect(serialized.content).toMatchObject({
[M_MESSAGE.name]: [
{ body: "Text here", mimetype: "text/plain" },
{ body: "HTML here", mimetype: "text/html" },
],
body: "Text here",
msgtype: "m.text",
format: "org.matrix.custom.html",
formatted_body: "HTML here",
});
});
it("should serialize non-html content to a legacy fallback", () => {
const message = MessageEvent.from("Text here");
expect(message.text).toBe("Text here");
expect(message.renderings.length).toBe(1);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
const serialized = message.serialize();
expect(serialized.type).toBe("m.room.message");
expect(serialized.content).toMatchObject({
[M_TEXT.name]: "Text here",
body: "Text here",
msgtype: "m.text",
format: undefined,
formatted_body: undefined,
});
});
});
});
@@ -0,0 +1,107 @@
/*
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.
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 { PollEndEventContent, M_POLL_END } from "../../../src/@types/polls";
import { IPartialEvent, REFERENCE_RELATION, M_TEXT } from "../../../src/@types/extensible_events";
import { PollEndEvent } from "../../../src/extensible_events_v1/PollEndEvent";
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
describe("PollEndEvent", () => {
// Note: throughout these tests we don't really bother testing that
// MessageEvent is doing its job. It has its own tests to worry about.
it("should parse a poll closure", () => {
const input: IPartialEvent<PollEndEventContent> = {
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_END.name]: {},
[M_TEXT.name]: "Poll closed",
},
};
const event = new PollEndEvent(input);
expect(event.pollEventId).toBe("$poll");
expect(event.closingMessage.text).toBe("Poll closed");
});
it("should fail to parse a missing relationship", () => {
const input: IPartialEvent<PollEndEventContent> = {
type: M_POLL_END.name,
content: {
[M_POLL_END.name]: {},
[M_TEXT.name]: "Poll closed",
} as any, // force invalid type
};
expect(() => new PollEndEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
it("should fail to parse a missing relationship event ID", () => {
const input: IPartialEvent<PollEndEventContent> = {
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
},
[M_POLL_END.name]: {},
[M_TEXT.name]: "Poll closed",
} as any, // force invalid type
};
expect(() => new PollEndEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
it("should fail to parse an improper relationship", () => {
const input: IPartialEvent<PollEndEventContent> = {
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: "org.example.not-relationship",
event_id: "$poll",
},
[M_POLL_END.name]: {},
[M_TEXT.name]: "Poll closed",
} as any, // force invalid type
};
expect(() => new PollEndEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
describe("from & serialize", () => {
it("should serialize to a poll end event", () => {
const event = PollEndEvent.from("$poll", "Poll closed");
expect(event.pollEventId).toBe("$poll");
expect(event.closingMessage.text).toBe("Poll closed");
const serialized = event.serialize();
expect(M_POLL_END.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_END.name]: {},
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
});
});
});
});
@@ -0,0 +1,257 @@
/*
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.
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 { M_TEXT, IPartialEvent, REFERENCE_RELATION } from "../../../src/@types/extensible_events";
import {
M_POLL_START,
M_POLL_KIND_DISCLOSED,
PollResponseEventContent,
M_POLL_RESPONSE,
} from "../../../src/@types/polls";
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
import { PollResponseEvent } from "../../../src/extensible_events_v1/PollResponseEvent";
const SAMPLE_POLL = new PollStartEvent({
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
{ id: "one", [M_TEXT.name]: "ONE" },
{ id: "two", [M_TEXT.name]: "TWO" },
{ id: "thr", [M_TEXT.name]: "THR" },
],
},
},
});
describe("PollResponseEvent", () => {
it("should parse a poll response", () => {
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: ["one"],
},
},
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(false);
expect(response.answerIds).toMatchObject(["one"]);
expect(response.pollEventId).toBe("$poll");
});
it("should fail to parse a missing relationship", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
[M_POLL_RESPONSE.name]: {
answers: ["one"],
},
} as any, // force invalid type
};
expect(() => new PollResponseEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
it("should fail to parse a missing relationship event ID", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
},
[M_POLL_RESPONSE.name]: {
answers: ["one"],
},
} as any, // force invalid type
};
expect(() => new PollResponseEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
it("should fail to parse an improper relationship", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: "org.example.not-relationship",
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: ["one"],
},
} as any, // force invalid type
};
expect(() => new PollResponseEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
describe("validateAgainst", () => {
it("should spoil the vote when no answers", () => {
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]: {},
} as any, // force invalid type
};
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 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,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: [1, 2, 3],
},
} as any, // force invalid type
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(true);
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(true);
});
describe("consumer usage", () => {
it("should spoil the vote when invalid answers are given", () => {
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: ["A", "B", "C"],
},
},
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(false); // it won't know better
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(true);
});
it("should truncate answers to the poll max selections", () => {
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: ["one", "two", "thr"],
},
},
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(false); // it won't know better
expect(response.answerIds).toMatchObject(["one", "two", "thr"]);
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(false);
expect(response.answerIds).toMatchObject(["one", "two"]);
});
});
});
describe("from & serialize", () => {
it("should serialize to a poll response event", () => {
const response = PollResponseEvent.from(["A", "B", "C"], "$poll");
expect(response.spoiled).toBe(false);
expect(response.answerIds).toMatchObject(["A", "B", "C"]);
expect(response.pollEventId).toBe("$poll");
const serialized = response.serialize();
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: ["A", "B", "C"],
},
});
});
it("should serialize a spoiled vote", () => {
const response = PollResponseEvent.from([], "$poll");
expect(response.spoiled).toBe(true);
expect(response.answerIds).toMatchObject([]);
expect(response.pollEventId).toBe("$poll");
const serialized = response.serialize();
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: undefined,
},
});
});
});
});
@@ -0,0 +1,337 @@
/*
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.
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 { M_TEXT, IPartialEvent } from "../../../src/@types/extensible_events";
import {
M_POLL_START,
M_POLL_KIND_DISCLOSED,
PollAnswer,
PollStartEventContent,
M_POLL_KIND_UNDISCLOSED,
} from "../../../src/@types/polls";
import { PollStartEvent, PollAnswerSubevent } from "../../../src/extensible_events_v1/PollStartEvent";
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
describe("PollAnswerSubevent", () => {
// Note: throughout these tests we don't really bother testing that
// MessageEvent is doing its job. It has its own tests to worry about.
it("should parse an answer representation", () => {
const input: IPartialEvent<PollAnswer> = {
type: "org.matrix.sdk.poll.answer",
content: {
id: "one",
[M_TEXT.name]: "ONE",
},
};
const answer = new PollAnswerSubevent(input);
expect(answer.id).toBe("one");
expect(answer.text).toBe("ONE");
});
it("should fail to parse answers without an ID", () => {
const input: IPartialEvent<PollAnswer> = {
type: "org.matrix.sdk.poll.answer",
content: {
[M_TEXT.name]: "ONE",
} as any, // force invalid type
};
expect(() => new PollAnswerSubevent(input)).toThrow(
new InvalidEventError("Answer ID must be a non-empty string"),
);
});
it("should fail to parse answers without text", () => {
const input: IPartialEvent<PollAnswer> = {
type: "org.matrix.sdk.poll.answer",
content: {
id: "one",
} as any, // force invalid type
};
expect(() => new PollAnswerSubevent(input)).toThrow(); // we don't check message - that'll be MessageEvent's problem
});
describe("from & serialize", () => {
it("should serialize to a placeholder representation", () => {
const answer = PollAnswerSubevent.from("one", "ONE");
expect(answer.id).toBe("one");
expect(answer.text).toBe("ONE");
const serialized = answer.serialize();
expect(serialized.type).toBe("org.matrix.sdk.poll.answer");
expect(serialized.content).toMatchObject({
id: "one",
[M_TEXT.name]: expect.any(String), // tested by MessageEvent
});
});
});
});
describe("PollStartEvent", () => {
// Note: throughout these tests we don't really bother testing that
// MessageEvent is doing its job. It has its own tests to worry about.
it("should parse a poll", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
{ id: "one", [M_TEXT.name]: "ONE" },
{ id: "two", [M_TEXT.name]: "TWO" },
{ id: "thr", [M_TEXT.name]: "THR" },
],
},
},
};
const poll = new PollStartEvent(input);
expect(poll.question).toBeDefined();
expect(poll.question.text).toBe("Question here");
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
expect(poll.maxSelections).toBe(2);
expect(poll.answers.length).toBe(3);
expect(poll.answers.some((a) => a.id === "one" && a.text === "ONE")).toBe(true);
expect(poll.answers.some((a) => a.id === "two" && a.text === "TWO")).toBe(true);
expect(poll.answers.some((a) => a.id === "thr" && a.text === "THR")).toBe(true);
});
it("should fail to parse a missing question", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
{ id: "one", [M_TEXT.name]: "ONE" },
{ id: "two", [M_TEXT.name]: "TWO" },
{ id: "thr", [M_TEXT.name]: "THR" },
],
},
} as any, // force invalid type
};
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("A question is required"));
});
it("should fail to parse non-array answers", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: "one",
} as any, // force invalid type
},
};
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("Poll answers must be an array"));
});
it("should fail to parse invalid answers", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [{ id: "one" }, { [M_TEXT.name]: "TWO" }],
} as any, // force invalid type
},
};
expect(() => new PollStartEvent(input)).toThrow(); // error tested by PollAnswerSubevent tests
});
it("should fail to parse lack of answers", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [],
} as any, // force invalid type
},
};
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("No answers available"));
});
it("should truncate answers at 20", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
{ id: "01", [M_TEXT.name]: "A" },
{ id: "02", [M_TEXT.name]: "B" },
{ id: "03", [M_TEXT.name]: "C" },
{ id: "04", [M_TEXT.name]: "D" },
{ id: "05", [M_TEXT.name]: "E" },
{ id: "06", [M_TEXT.name]: "F" },
{ id: "07", [M_TEXT.name]: "G" },
{ id: "08", [M_TEXT.name]: "H" },
{ id: "09", [M_TEXT.name]: "I" },
{ id: "10", [M_TEXT.name]: "J" },
{ id: "11", [M_TEXT.name]: "K" },
{ id: "12", [M_TEXT.name]: "L" },
{ id: "13", [M_TEXT.name]: "M" },
{ id: "14", [M_TEXT.name]: "N" },
{ id: "15", [M_TEXT.name]: "O" },
{ id: "16", [M_TEXT.name]: "P" },
{ id: "17", [M_TEXT.name]: "Q" },
{ id: "18", [M_TEXT.name]: "R" },
{ id: "19", [M_TEXT.name]: "S" },
{ id: "20", [M_TEXT.name]: "T" },
{ id: "FAIL", [M_TEXT.name]: "U" },
],
},
},
};
const poll = new PollStartEvent(input);
expect(poll.answers.length).toBe(20);
expect(poll.answers.some((a) => a.id === "FAIL")).toBe(false);
});
it("should infer a kind from unknown kinds", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: "org.example.custom.poll.kind",
max_selections: 2,
answers: [
{ id: "01", [M_TEXT.name]: "A" },
{ id: "02", [M_TEXT.name]: "B" },
{ id: "03", [M_TEXT.name]: "C" },
],
},
},
};
const poll = new PollStartEvent(input);
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
expect(poll.rawKind).toBe("org.example.custom.poll.kind");
});
it("should infer a kind from missing kinds", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
max_selections: 2,
answers: [
{ id: "01", [M_TEXT.name]: "A" },
{ id: "02", [M_TEXT.name]: "B" },
{ id: "03", [M_TEXT.name]: "C" },
],
} as any, // force invalid type
},
};
const poll = new PollStartEvent(input);
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
expect(poll.rawKind).toBeFalsy();
});
describe("from & serialize", () => {
it("should serialize to a poll start event", () => {
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], M_POLL_KIND_DISCLOSED, 2);
expect(poll.question.text).toBe("Question here");
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
expect(poll.maxSelections).toBe(2);
expect(poll.answers.length).toBe(3);
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
// Ids are non-empty and unique
expect(poll.answers[0].id).toHaveLength(16);
expect(poll.answers[1].id).toHaveLength(16);
expect(poll.answers[2].id).toHaveLength(16);
expect(poll.answers[0].id).not.toEqual(poll.answers[1].id);
expect(poll.answers[0].id).not.toEqual(poll.answers[2].id);
expect(poll.answers[1].id).not.toEqual(poll.answers[2].id);
const serialized = poll.serialize();
expect(M_POLL_START.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
[M_POLL_START.name]: {
question: {
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
},
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
// M_TEXT tested by MessageEvent tests
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
],
},
});
});
it("should serialize to a custom kind poll start event", () => {
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], "org.example.poll.kind", 2);
expect(poll.question.text).toBe("Question here");
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
expect(poll.rawKind).toBe("org.example.poll.kind");
expect(poll.maxSelections).toBe(2);
expect(poll.answers.length).toBe(3);
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
const serialized = poll.serialize();
expect(M_POLL_START.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
[M_POLL_START.name]: {
question: {
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
},
kind: "org.example.poll.kind",
max_selections: 2,
answers: [
// M_MESSAGE tested by MessageEvent tests
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
],
},
});
});
});
});
@@ -0,0 +1,87 @@
/*
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.
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 { NamespacedValue } from "matrix-events-sdk";
import { isEventTypeSame } from "../../../src/@types/extensible_events";
describe("isEventTypeSame", () => {
it("should match string and string", () => {
const a = "org.example.message-like";
const b = "org.example.different";
expect(isEventTypeSame(a, b)).toBe(false);
expect(isEventTypeSame(b, a)).toBe(false);
expect(isEventTypeSame(a, a)).toBe(true);
expect(isEventTypeSame(b, b)).toBe(true);
});
it("should match string and namespace", () => {
const a = "org.example.message-like";
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
expect(isEventTypeSame(a, b)).toBe(false);
expect(isEventTypeSame(b, a)).toBe(false);
expect(isEventTypeSame(a, a)).toBe(true);
expect(isEventTypeSame(b, b)).toBe(true);
expect(isEventTypeSame(b.name, b)).toBe(true);
expect(isEventTypeSame(b.altName, b)).toBe(true);
expect(isEventTypeSame(b, b.name)).toBe(true);
expect(isEventTypeSame(b, b.altName)).toBe(true);
});
it("should match namespace and namespace", () => {
const a = new NamespacedValue<string, string>("org.example.stable1", "org.example.unstable1");
const b = new NamespacedValue<string, string>("org.example.stable2", "org.example.unstable2");
expect(isEventTypeSame(a, b)).toBe(false);
expect(isEventTypeSame(b, a)).toBe(false);
expect(isEventTypeSame(a, a)).toBe(true);
expect(isEventTypeSame(a.name, a)).toBe(true);
expect(isEventTypeSame(a.altName, a)).toBe(true);
expect(isEventTypeSame(a, a.name)).toBe(true);
expect(isEventTypeSame(a, a.altName)).toBe(true);
expect(isEventTypeSame(b, b)).toBe(true);
expect(isEventTypeSame(b.name, b)).toBe(true);
expect(isEventTypeSame(b.altName, b)).toBe(true);
expect(isEventTypeSame(b, b.name)).toBe(true);
expect(isEventTypeSame(b, b.altName)).toBe(true);
});
it("should match namespaces of different pointers", () => {
const a = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
expect(isEventTypeSame(a, b)).toBe(true);
expect(isEventTypeSame(b, a)).toBe(true);
expect(isEventTypeSame(a, a)).toBe(true);
expect(isEventTypeSame(a.name, a)).toBe(true);
expect(isEventTypeSame(a.altName, a)).toBe(true);
expect(isEventTypeSame(a, a.name)).toBe(true);
expect(isEventTypeSame(a, a.altName)).toBe(true);
expect(isEventTypeSame(b, b)).toBe(true);
expect(isEventTypeSame(b.name, b)).toBe(true);
expect(isEventTypeSame(b.altName, b)).toBe(true);
expect(isEventTypeSame(b, b.name)).toBe(true);
expect(isEventTypeSame(b, b.altName)).toBe(true);
});
});
-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 () => {
+4 -4
View File
@@ -22,7 +22,7 @@ import {
M_TIMESTAMP,
LocationEventWireContent,
} from "../../src/@types/location";
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
import { M_TEXT } from "../../src/@types/extensible_events";
import { MsgType } from "../../src/@types/event";
describe("Location", function () {
@@ -32,7 +32,7 @@ describe("Location", function () {
geo_uri: "geo:-36.24484561954707,175.46884959563613;u=10",
[M_LOCATION.name]: { uri: "geo:-36.24484561954707,175.46884959563613;u=10", description: null },
[M_ASSET.name]: { type: "m.self" },
[TEXT_NODE_TYPE.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
[M_TEXT.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
[M_TIMESTAMP.name]: 1646823712443,
} as any;
@@ -59,7 +59,7 @@ describe("Location", function () {
description: undefined,
});
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Self });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("User Location geo:foo at 1970-01-02T13:17:15.435Z");
expect(M_TEXT.findIn(loc)).toEqual("User Location geo:foo at 1970-01-02T13:17:15.435Z");
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235435);
});
@@ -74,7 +74,7 @@ describe("Location", function () {
description: "desc",
});
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
expect(M_TEXT.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235436);
});
File diff suppressed because it is too large Load Diff
+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", () => {
+1 -2
View File
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
import { MatrixEvent } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import { isTimestampInDuration, Beacon, BeaconEvent } from "../../../src/models/beacon";
+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 });
});
});
});
});
+486
View File
@@ -0,0 +1,486 @@
/*
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 { 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 { 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();
describe("Poll", () => {
const userId = "@alice:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
decryptEventIfNeeded: jest.fn().mockResolvedValue(true),
relations: jest.fn(),
});
const roomId = "!room:server";
const room = new Room(roomId, mockClient, userId);
const maySendRedactionForEventSpy = jest.spyOn(room.currentState, "maySendRedactionForEvent");
// 14.03.2022 16:15
const now = 1647270879403;
const basePollStartEvent = new MatrixEvent({
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
room_id: roomId,
sender: userId,
});
basePollStartEvent.event.event_id = "$12345";
beforeEach(() => {
jest.clearAllMocks();
jest.setSystemTime(now);
mockClient.relations.mockReset().mockResolvedValue({ events: [] });
maySendRedactionForEventSpy.mockClear().mockReturnValue(true);
});
let eventId = 1;
const makeRelatedEvent = (eventProps: Partial<IEvent>, timestamp = now): MatrixEvent => {
const event = new MatrixEvent({
...eventProps,
content: {
...(eventProps.content || {}),
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: basePollStartEvent.getId(),
},
},
});
event.event.origin_server_ts = timestamp;
event.event.event_id = `${eventId++}`;
return event;
};
it("initialises with root event", () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
expect(poll.roomId).toEqual(roomId);
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)).toThrow("Invalid poll start event.");
});
it("throws when poll start has no event id", () => {
const pollStartEvent = new MatrixEvent({
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
room_id: roomId,
});
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
});
describe("fetching responses", () => {
it("calls relations api and emits", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
const emitSpy = jest.spyOn(poll, "emit");
const fetchResponsePromise = poll.getResponses();
expect(poll.isFetchingResponses).toBe(true);
const responses = await fetchResponsePromise;
expect(poll.isFetchingResponses).toBe(false);
expect(mockClient.relations).toHaveBeenCalledWith(
roomId,
basePollStartEvent.getId(),
"m.reference",
undefined,
{ from: undefined },
);
expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses);
});
it("returns existing responses object after initial fetch", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
const responses = await poll.getResponses();
const responses2 = await poll.getResponses();
// only fetched relations once
expect(mockClient.relations).toHaveBeenCalledTimes(1);
// strictly equal
expect(responses).toBe(responses2);
});
it("waits for existing relations request to finish when getting responses", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
const firstResponsePromise = poll.getResponses();
const secondResponsePromise = poll.getResponses();
await firstResponsePromise;
expect(firstResponsePromise).toEqual(secondResponsePromise);
await secondResponsePromise;
expect(mockClient.relations).toHaveBeenCalledTimes(1);
});
it("filters relations for relevent response events", async () => {
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
mockClient.relations.mockResolvedValue({
events: [replyEvent, stableResponseEvent, unstableResponseEvent],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
const responses = await poll.getResponses();
expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]);
});
describe("with multiple pages of relations", () => {
const makeResponses = (count = 1, timestamp = now): MatrixEvent[] =>
new Array(count)
.fill("x")
.map((_x, index) =>
makeRelatedEvent(
{ type: M_POLL_RESPONSE.stable!, sender: "@bob@server.org" },
timestamp + index,
),
);
it("page relations responses", async () => {
const responseEvents = makeResponses(6);
mockClient.relations
.mockResolvedValueOnce({
events: responseEvents.slice(0, 2),
nextBatch: "test-next-1",
})
.mockResolvedValueOnce({
events: responseEvents.slice(2, 4),
nextBatch: "test-next-2",
})
.mockResolvedValueOnce({
events: responseEvents.slice(4),
});
const poll = new Poll(basePollStartEvent, mockClient, room);
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" }],
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-2" }],
]);
expect(poll.emit).toHaveBeenCalledTimes(3);
expect(poll.isFetchingResponses).toBeFalsy();
expect(responses.getRelations().length).toEqual(6);
});
});
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" });
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);
beforeEach(() => {
mockClient.relations.mockResolvedValue({
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, stablePollEndEvent],
});
});
it("sets poll end event with stable event type", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
await poll.getResponses();
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
expect(poll.isEnded).toBe(true);
expect(poll.endEventId).toBe(stablePollEndEvent.getId()!);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
});
it("sets poll end event when endevent sender also created the poll, but does not have redaction rights", async () => {
const pollStartEvent = new MatrixEvent({
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
room_id: roomId,
sender: "@bob:domain.org",
});
pollStartEvent.event.event_id = "$6789";
const poll = new Poll(pollStartEvent, mockClient, room);
const pollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob:domain.org" });
mockClient.relations.mockResolvedValue({
events: [pollEndEvent],
});
maySendRedactionForEventSpy.mockReturnValue(false);
jest.spyOn(poll, "emit");
await poll.getResponses();
expect(maySendRedactionForEventSpy).not.toHaveBeenCalled();
expect(poll.isEnded).toBe(true);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
});
it("sets poll end event with unstable event type", async () => {
mockClient.relations.mockResolvedValue({
events: [unstablePollEndEvent],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
await poll.getResponses();
expect(poll.isEnded).toBe(true);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
});
it("filters out responses that were sent after poll end", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
const responses = await poll.getResponses();
// just response type events
// and response with ts after poll end event is excluded
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
});
});
});
describe("onNewRelation()", () => {
it("discards response if poll responses have not been initialised", () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
poll.onNewRelation(responseEvent);
// did not add response -> no emit
expect(poll.emit).not.toHaveBeenCalled();
});
it("sets poll end event when responses are not initialised", () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
jest.spyOn(poll, "emit");
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
poll.onNewRelation(stablePollEndEvent);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
});
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("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("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("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);
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],
});
const poll = new Poll(basePollStartEvent, mockClient, room);
const responses = await poll.getResponses();
jest.spyOn(poll, "emit");
expect(responses.getRelations().length).toEqual(3);
poll.onNewRelation(stablePollEndEvent);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
expect(responses.getRelations().length).toEqual(2);
// after end timestamp event is removed
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
});
it("filters out irrelevant relations", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
// init responses
const responses = await poll.getResponses();
jest.spyOn(poll, "emit");
const replyEvent = new MatrixEvent({ type: "m.room.message" });
poll.onNewRelation(replyEvent);
// did not add response -> no emit
expect(poll.emit).not.toHaveBeenCalled();
expect(responses.getRelations().length).toEqual(0);
});
it("adds poll response relations to responses", async () => {
const poll = new Poll(basePollStartEvent, mockClient, room);
// init responses
const responses = await poll.getResponses();
jest.spyOn(poll, "emit");
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
poll.onNewRelation(responseEvent);
// did not add response -> no emit
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
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);
},
);
});
});
+144 -3
View File
@@ -20,7 +20,7 @@ import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/t
import { mkThread } from "../../test-utils/thread";
import { TestClient } from "../../TestClient";
import { emitPromise, mkMessage, mock } from "../../test-utils/test-utils";
import { EventStatus, MatrixEvent } from "../../../src";
import { Direction, EventStatus, MatrixEvent } from "../../../src";
import { ReceiptType } from "../../../src/@types/read_receipts";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
import { ReEmitter } from "../../../src/ReEmitter";
@@ -82,9 +82,10 @@ describe("Thread", () => {
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
isInitialSyncComplete: jest.fn().mockReturnValue(false),
getRoom: jest.fn().mockImplementation(() => room),
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
supportsThreads: jest.fn().mockReturnValue(true),
});
client.reEmitter = mock(ReEmitter, "ReEmitter");
client.canSupport = new Map();
@@ -193,9 +194,10 @@ describe("Thread", () => {
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
isInitialSyncComplete: jest.fn().mockReturnValue(false),
getRoom: jest.fn().mockImplementation(() => room),
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
supportsThreads: jest.fn().mockReturnValue(true),
});
client.reEmitter = mock(ReEmitter, "ReEmitter");
client.canSupport = new Map();
@@ -283,4 +285,143 @@ describe("Thread", () => {
expect(thread2.getEventReadUpTo(myUserId)).toBe(null);
});
});
describe("resetLiveTimeline", () => {
// ResetLiveTimeline is used when we have missing messages between the current live timeline's end and newly
// received messages. In that case, we want to replace the existing live timeline. To ensure pagination
// continues working correctly, new pagination tokens need to be set on both the old live timeline (which is
// now a regular timeline) and the new live timeline.
it("replaces the live timeline and correctly sets pagination tokens", async () => {
const myUserId = "@bob:example.org";
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
timelineSupport: false,
});
const client = testClient.client;
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
const { thread } = mkThread({
room,
client,
authorId: myUserId,
participantUserIds: ["@alice:example.org"],
length: 3,
});
await emitPromise(thread, ThreadEvent.Update);
expect(thread.length).toBe(2);
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
Promise.resolve({
chunk: [],
start: `${token}-new`,
end: `${token}-new`,
}),
);
function timelines(): [string | null, string | null][] {
return thread.timelineSet
.getTimelines()
.map((it) => [it.getPaginationToken(Direction.Backward), it.getPaginationToken(Direction.Forward)]);
}
expect(timelines()).toEqual([[null, null]]);
const promise = thread.resetLiveTimeline("b1", "f1");
expect(timelines()).toEqual([
[null, "f1"],
["b1", null],
]);
await promise;
expect(timelines()).toEqual([
[null, "f1-new"],
["b1-new", null],
]);
});
// As the pagination tokens cannot be used right now, resetLiveTimeline needs to replace them before they can
// be used. But if in the future the bug in synapse is fixed, and they can actually be used, we can get into a
// state where the client has paginated (and changed the tokens) while resetLiveTimeline tries to set the
// corrected tokens. To prevent such a race condition, we make sure that resetLiveTimeline respects any
// changes done to the pagination tokens.
it("replaces the live timeline but does not replace changed pagination tokens", async () => {
const myUserId = "@bob:example.org";
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
timelineSupport: false,
});
const client = testClient.client;
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
const { thread } = mkThread({
room,
client,
authorId: myUserId,
participantUserIds: ["@alice:example.org"],
length: 3,
});
await emitPromise(thread, ThreadEvent.Update);
expect(thread.length).toBe(2);
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
Promise.resolve({
chunk: [],
start: `${token}-new`,
end: `${token}-new`,
}),
);
function timelines(): [string | null, string | null][] {
return thread.timelineSet
.getTimelines()
.map((it) => [it.getPaginationToken(Direction.Backward), it.getPaginationToken(Direction.Forward)]);
}
expect(timelines()).toEqual([[null, null]]);
const promise = thread.resetLiveTimeline("b1", "f1");
expect(timelines()).toEqual([
[null, "f1"],
["b1", null],
]);
thread.timelineSet.getTimelines()[0].setPaginationToken("f2", Direction.Forward);
thread.timelineSet.getTimelines()[1].setPaginationToken("b2", Direction.Backward);
await promise;
expect(timelines()).toEqual([
[null, "f2"],
["b2", null],
]);
});
it("is correctly called by the room", async () => {
const myUserId = "@bob:example.org";
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
timelineSupport: false,
});
const client = testClient.client;
const room = new Room("123", client, myUserId, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
jest.spyOn(client, "getRoom").mockReturnValue(room);
const { thread } = mkThread({
room,
client,
authorId: myUserId,
participantUserIds: ["@alice:example.org"],
length: 3,
});
await emitPromise(thread, ThreadEvent.Update);
expect(thread.length).toBe(2);
const mock = jest.spyOn(thread, "resetLiveTimeline");
mock.mockReturnValue(Promise.resolve());
room.resetLiveTimeline("b1", "f1");
expect(mock).toHaveBeenCalledWith("b1", "f1");
});
});
});
+30 -4
View File
@@ -53,10 +53,11 @@ describe("fixNotificationCountOnDecryption", () => {
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
isInitialSyncComplete: jest.fn().mockReturnValue(false),
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
getRoom: jest.fn().mockImplementation(() => room),
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
supportsThreads: jest.fn().mockReturnValue(true),
});
mockClient.reEmitter = mock(ReEmitter, "ReEmitter");
mockClient.canSupport = new Map();
@@ -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);
});
});
-52
View File
@@ -18,7 +18,6 @@ import MockHttpBackend from "matrix-mock-request";
import { MAIN_ROOM_TIMELINE, ReceiptType } from "../../src/@types/read_receipts";
import { MatrixClient } from "../../src/client";
import { Feature, ServerSupport } from "../../src/feature";
import { EventType } from "../../src/matrix";
import { synthesizeReceipt } from "../../src/models/read-receipt";
import { encodeUri } from "../../src/utils";
@@ -70,10 +69,6 @@ const roomEvent = utils.mkEvent({
},
});
function mockServerSideSupport(client: MatrixClient, serverSideSupport: ServerSupport) {
client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport);
}
describe("Read receipt", () => {
beforeEach(() => {
httpBackend = new MockHttpBackend();
@@ -101,7 +96,6 @@ describe("Read receipt", () => {
})
.respond(200, {});
mockServerSideSupport(client, ServerSupport.Stable);
client.sendReceipt(threadEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
@@ -123,7 +117,6 @@ describe("Read receipt", () => {
})
.respond(200, {});
mockServerSideSupport(client, ServerSupport.Stable);
client.sendReadReceipt(threadEvent, ReceiptType.Read, true);
await httpBackend.flushAllExpected();
@@ -145,56 +138,11 @@ describe("Read receipt", () => {
})
.respond(200, {});
mockServerSideSupport(client, ServerSupport.Stable);
client.sendReceipt(roomEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends a room read receipt when there's no server support", async () => {
httpBackend
.when(
"POST",
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId()!,
}),
)
.check((request) => {
expect(request.data.thread_id).toBeUndefined();
})
.respond(200, {});
mockServerSideSupport(client, ServerSupport.Unsupported);
client.sendReceipt(threadEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends a valid room read receipt even when body omitted", async () => {
httpBackend
.when(
"POST",
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId()!,
}),
)
.check((request) => {
expect(request.data).toEqual({});
})
.respond(200, {});
mockServerSideSupport(client, ServerSupport.Unsupported);
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
await httpBackend.flushAllExpected();
await flushPromises();
});
});
describe("synthesizeReceipt", () => {
+1 -2
View File
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { M_POLL_START } from "matrix-events-sdk";
import { M_POLL_START } from "../../src/@types/polls";
import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
@@ -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]);
});
+227 -35
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.
@@ -19,6 +19,7 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
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";
@@ -37,6 +38,7 @@ import {
MatrixEvent,
MatrixEventEvent,
PendingEventOrdering,
PollEvent,
RelationType,
RoomEvent,
RoomMember,
@@ -51,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";
@@ -169,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", () => {
@@ -388,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 () {
@@ -786,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 () {
@@ -1622,7 +1625,7 @@ describe("Room", function () {
describe("addPendingEvent", function () {
it("should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", function () {
const client = new TestClient("@alice:example.com", "alicedevice").client;
client.supportsExperimentalThreads = () => true;
client.supportsThreads = () => true;
const room = new Room(roomId, client, userA, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
@@ -2468,7 +2471,7 @@ describe("Room", function () {
});
it("Edits update the lastReply event", async () => {
room.client.supportsExperimentalThreads = () => true;
room.client.supportsThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const randomMessage = mkMessage();
@@ -2539,7 +2542,7 @@ describe("Room", function () {
});
it("Redactions to thread responses decrement the length", async () => {
room.client.supportsExperimentalThreads = () => true;
room.client.supportsThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
@@ -2606,7 +2609,7 @@ describe("Room", function () {
});
it("Redactions to reactions in threads do not decrement the length", async () => {
room.client.supportsExperimentalThreads = () => true;
room.client.supportsThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
@@ -2646,7 +2649,7 @@ describe("Room", function () {
});
it("should not decrement the length when the thread root is redacted", async () => {
room.client.supportsExperimentalThreads = () => true;
room.client.supportsThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
@@ -2687,7 +2690,7 @@ describe("Room", function () {
});
it("Redacting the lastEvent finds a new lastEvent", async () => {
room.client.supportsExperimentalThreads = () => true;
room.client.supportsThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
Thread.setServerSideListSupport(FeatureSupport.Stable);
@@ -2794,7 +2797,7 @@ describe("Room", function () {
describe("eventShouldLiveIn", () => {
const client = new TestClient(userA).client;
client.supportsExperimentalThreads = () => true;
client.supportsThreads = () => true;
Thread.setServerSideSupport(FeatureSupport.Stable);
const room = new Room(roomId, client, userA);
@@ -3228,12 +3231,125 @@ describe("Room", function () {
});
});
describe("processPollEvents()", () => {
let room: Room;
let client: MatrixClient;
beforeEach(() => {
client = getMockClientWithEventEmitter({
decryptEventIfNeeded: jest.fn(),
});
room = new Room(roomId, client, userA);
jest.spyOn(room, "emit").mockClear();
});
const makePollStart = (id: string): MatrixEvent => {
const event = new MatrixEvent({
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
room_id: roomId,
});
event.event.event_id = id;
return event;
};
it("adds poll models to room state for a poll start event", async () => {
const pollStartEvent = makePollStart("1");
const events = [pollStartEvent];
await room.processPollEvents(events);
expect(client.decryptEventIfNeeded).toHaveBeenCalledWith(pollStartEvent);
const pollInstance = room.polls.get(pollStartEvent.getId()!);
expect(pollInstance).toBeTruthy();
expect(room.emit).toHaveBeenCalledWith(PollEvent.New, pollInstance);
});
it("adds related events to poll models and log errors", async () => {
const pollStartEvent = makePollStart("1");
const pollStartEvent2 = makePollStart("2");
const events = [pollStartEvent, pollStartEvent2];
const pollResponseEvent = new MatrixEvent({
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: RelationType.Reference,
event_id: pollStartEvent.getId(),
},
},
});
const messageEvent = new MatrixEvent({
type: "m.room.messsage",
content: {
text: "hello",
},
});
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);
const poll = room.polls.get(pollStartEvent.getId()!)!;
const poll2 = room.polls.get(pollStartEvent2.getId()!)!;
jest.spyOn(poll, "onNewRelation");
jest.spyOn(poll2, "onNewRelation");
await room.processPollEvents([errorEvent, messageEvent, pollResponseEvent]);
// only called for relevant event
expect(poll.onNewRelation).toHaveBeenCalledTimes(1);
expect(poll.onNewRelation).toHaveBeenCalledWith(pollResponseEvent);
// 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);
});
});
describe("findPredecessorRoomId", () => {
let client: MatrixClient | null = null;
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
isInitialSyncComplete: jest.fn().mockReturnValue(false),
supportsThreads: jest.fn().mockReturnValue(true),
});
});
@@ -3266,21 +3382,97 @@ describe("Room", function () {
});
}
function predecessorEvent(
newRoomId: string,
predecessorRoomId: string,
tombstoneEventId: string | null = null,
viaServers: string[] = [],
): MatrixEvent {
const content =
tombstoneEventId === null
? { predecessor_room_id: predecessorRoomId, via_servers: viaServers }
: {
predecessor_room_id: predecessorRoomId,
last_known_event_id: tombstoneEventId,
via_servers: viaServers,
};
return new MatrixEvent({
content,
event_id: `predecessor_event_id_pred_${predecessorRoomId}`,
origin_server_ts: 1432735824653,
room_id: newRoomId,
sender: "@daryl:alexandria.example.com",
state_key: "",
type: "org.matrix.msc3946.room_predecessor",
});
}
it("Returns null if there is no create event", () => {
const room = new Room("roomid", client!, "@u:example.com");
expect(room.findPredecessorRoomId()).toBeNull();
expect(room.findPredecessor()).toBeNull();
});
it("Returns null if the create event has no predecessor", () => {
const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([roomCreateEvent("roomid", null)]);
expect(room.findPredecessorRoomId()).toBeNull();
expect(room.findPredecessor()).toBeNull();
});
it("Returns the predecessor ID if one is provided via create event", () => {
const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]);
expect(room.findPredecessorRoomId()).toBe("replacedroomid");
expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" });
});
it("Prefers the m.predecessor event if one exists", () => {
const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([
roomCreateEvent("roomid", "replacedroomid"),
predecessorEvent("roomid", "otherreplacedroomid"),
]);
const useMsc3946 = true;
expect(room.findPredecessor(useMsc3946)).toEqual({
roomId: "otherreplacedroomid",
eventId: undefined, // m.predecessor did not include an event_id
viaServers: [],
});
});
it("uses the m.predecessor event ID if provided", () => {
const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([
roomCreateEvent("roomid", "replacedroomid"),
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"],
});
});
it("Ignores the m.predecessor event if we don't ask to use it", () => {
const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([
roomCreateEvent("roomid", "replacedroomid"),
predecessorEvent("roomid", "otherreplacedroomid"),
]);
// Don't provide an argument for msc3946ProcessDynamicPredecessor -
// we should ignore the predecessor event.
expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" });
});
it("Ignores the m.predecessor event and returns null if we don't ask to use it", () => {
const room = new Room("roomid", client!, "@u:example.com");
room.addLiveEvents([
roomCreateEvent("roomid", null), // Create event has no predecessor
predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"),
]);
// Don't provide an argument for msc3946ProcessDynamicPredecessor -
// we should ignore the predecessor event.
expect(room.findPredecessor()).toBeNull();
});
});
});
@@ -0,0 +1,158 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import fetchMock from "fetch-mock-jest";
import { Mocked } from "jest-mock";
import { KeysClaimRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
afterEach(() => {
fetchMock.mockReset();
});
describe("KeyClaimManager", () => {
/* for these tests, we connect a KeyClaimManager to a mock OlmMachine, and a real OutgoingRequestProcessor
* (which is connected to a mock fetch implementation)
*/
/** the KeyClaimManager implementation under test */
let keyClaimManager: KeyClaimManager;
/** a mocked-up OlmMachine which the OutgoingRequestProcessor and KeyClaimManager are connected to */
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(async () => {
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
baseUrl: "https://example.com",
prefix: "/_matrix",
onlyData: true,
});
olmMachine = {
getMissingSessions: jest.fn(),
markRequestAsSent: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
const outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, httpApi);
keyClaimManager = new KeyClaimManager(olmMachine, outgoingRequestProcessor);
});
/**
* Returns a promise which resolve once olmMachine.markRequestAsSent is called.
*
* The call itself will block initially.
*
* The promise returned by this function yields a callback function, which should be called to unblock the
* markRequestAsSent call.
*/
function awaitCallToMarkRequestAsSent(): Promise<() => void> {
return new Promise<() => void>((resolveCalledPromise, _reject) => {
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
// the mock implementation returns a promise...
const completePromise = new Promise<void>((resolveCompletePromise, _reject) => {
// ... and we now resolve the original promise with the resolver for that second promise.
resolveCalledPromise(resolveCompletePromise);
});
return completePromise;
});
});
}
it("should claim missing keys", async () => {
const u1 = new UserId("@alice:example.com");
const u2 = new UserId("@bob:example.com");
// stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim
const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }');
olmMachine.getMissingSessions.mockResolvedValueOnce(keysClaimRequest);
// have the claim request return a 200
fetchMock.postOnce("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }');
// also stub out olmMachine.markRequestAsSent
olmMachine.markRequestAsSent.mockResolvedValueOnce(undefined);
// fire off the request
await keyClaimManager.ensureSessionsForUsers([u1, u2]);
// check that all the calls were made
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1, u2]);
expect(fetchMock).toHaveFetched("https://example.com/_matrix/client/v3/keys/claim", {
method: "POST",
body: { k1: "v1" },
});
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", keysClaimRequest.type, '{ "k": "v" }');
});
it("should wait for previous claims to complete before making another", async () => {
const u1 = new UserId("@alice:example.com");
const u2 = new UserId("@bob:example.com");
// stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim
const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }');
olmMachine.getMissingSessions.mockResolvedValue(keysClaimRequest);
// have the claim request return a 200
fetchMock.post("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }');
// stub out olmMachine.markRequestAsSent, and have it block
let markRequestAsSentPromise = awaitCallToMarkRequestAsSent();
// fire off two requests, and keep track of whether their promises resolve
let req1Resolved = false;
keyClaimManager.ensureSessionsForUsers([u1]).then(() => {
req1Resolved = true;
});
let req2Resolved = false;
const req2 = keyClaimManager.ensureSessionsForUsers([u2]).then(() => {
req2Resolved = true;
});
// now: wait for the (first) call to OlmMachine.markRequestAsSent
let resolveMarkRequestAsSentCallback = await markRequestAsSentPromise;
// at this point, there should have been a single call to getMissingSessions, and a single fetch; and neither
// call to ensureSessionsAsUsers should have completed
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1]);
expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(req1Resolved).toBe(false);
expect(req2Resolved).toBe(false);
// await the next call to markRequestAsSent, and release the first one
markRequestAsSentPromise = awaitCallToMarkRequestAsSent();
resolveMarkRequestAsSentCallback();
resolveMarkRequestAsSentCallback = await markRequestAsSentPromise;
// the first request should now have completed, and we should have more calls and fetches
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u2]);
expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(2);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(req1Resolved).toBe(true);
expect(req2Resolved).toBe(false);
// finally, release the second call to markRequestAsSent and check that the second request completes
resolveMarkRequestAsSentCallback();
await req2;
});
});
@@ -0,0 +1,180 @@
/*
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 MockHttpBackend from "matrix-mock-request";
import { Mocked } from "jest-mock";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import {
KeysBackupRequest,
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
RoomMessageRequest,
SignatureUploadRequest,
ToDeviceRequest,
} from "@matrix-org/matrix-sdk-crypto-js";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
describe("OutgoingRequestProcessor", () => {
/** the OutgoingRequestProcessor implementation under test */
let processor: OutgoingRequestProcessor;
/** A mock http backend which processor is connected to */
let httpBackend: MockHttpBackend;
/** a mocked-up OlmMachine which processor is connected to */
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
/** wait for a call to olmMachine.markRequestAsSent */
function awaitCallToMarkAsSent(): Promise<void> {
return new Promise((resolve, _reject) => {
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
resolve(undefined);
});
});
}
beforeEach(async () => {
httpBackend = new MockHttpBackend();
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
baseUrl: "https://example.com",
prefix: "/_matrix",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
onlyData: true,
});
olmMachine = {
markRequestAsSent: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
processor = new OutgoingRequestProcessor(olmMachine, httpApi);
});
/* simple requests that map directly to the request body */
const tests: Array<[string, any, "POST" | "PUT", string]> = [
["KeysUploadRequest", KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
["KeysQueryRequest", KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
["KeysClaimRequest", KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
[
"SignatureUploadRequest",
SignatureUploadRequest,
"POST",
"https://example.com/_matrix/client/v3/keys/signatures/upload",
],
["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
];
test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
// first, mock up a request as we might expect to receive it from the Rust layer ...
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RequestClass("1234", testBody);
// ... then poke it into the OutgoingRequestProcessor under test.
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
// Now: check that it makes a matching HTTP request ...
const testResponse = '{ "result": 1 }';
httpBackend
.when(expectedMethod, "/_matrix")
.check((req) => {
expect(req.path).toEqual(expectedPath);
expect(req.rawData).toEqual(testBody);
expect(req.headers["Accept"]).toEqual("application/json");
expect(req.headers["Content-Type"]).toEqual("application/json");
})
.respond(200, testResponse, true);
// ... and that it calls OlmMachine.markAsSent.
const markSentCallPromise = awaitCallToMarkAsSent();
await httpBackend.flushAllExpected();
await Promise.all([reqProm, markSentCallPromise]);
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
httpBackend.verifyNoOutstandingRequests();
});
it("should handle ToDeviceRequests", async () => {
// first, mock up the ToDeviceRequest as we might expect to receive it from the Rust layer ...
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new ToDeviceRequest("1234", "test/type", "test/txnid", testBody);
// ... then poke it into the OutgoingRequestProcessor under test.
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
// Now: check that it makes a matching HTTP request ...
const testResponse = '{ "result": 1 }';
httpBackend
.when("PUT", "/_matrix")
.check((req) => {
expect(req.path).toEqual("https://example.com/_matrix/client/v3/sendToDevice/test%2Ftype/test%2Ftxnid");
expect(req.rawData).toEqual(testBody);
expect(req.headers["Accept"]).toEqual("application/json");
expect(req.headers["Content-Type"]).toEqual("application/json");
})
.respond(200, testResponse, true);
// ... and that it calls OlmMachine.markAsSent.
const markSentCallPromise = awaitCallToMarkAsSent();
await httpBackend.flushAllExpected();
await Promise.all([reqProm, markSentCallPromise]);
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
httpBackend.verifyNoOutstandingRequests();
});
it("should handle RoomMessageRequests", async () => {
// first, mock up the RoomMessageRequest as we might expect to receive it from the Rust layer ...
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RoomMessageRequest("1234", "test/room", "test/txnid", "test/type", testBody);
// ... then poke it into the OutgoingRequestProcessor under test.
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
// Now: check that it makes a matching HTTP request ...
const testResponse = '{ "result": 1 }';
httpBackend
.when("PUT", "/_matrix")
.check((req) => {
expect(req.path).toEqual(
"https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid",
);
expect(req.rawData).toEqual(testBody);
expect(req.headers["Accept"]).toEqual("application/json");
expect(req.headers["Content-Type"]).toEqual("application/json");
})
.respond(200, testResponse, true);
// ... and that it calls OlmMachine.markAsSent.
const markSentCallPromise = awaitCallToMarkAsSent();
await httpBackend.flushAllExpected();
await Promise.all([reqProm, markSentCallPromise]);
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
httpBackend.verifyNoOutstandingRequests();
});
it("does not explode with unknown requests", async () => {
const outgoingRequest = { id: "5678", type: 987 };
const markSentCallPromise = awaitCallToMarkAsSent();
await Promise.all([processor.makeOutgoingRequest(outgoingRequest), markSentCallPromise]);
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
});
});
@@ -17,21 +17,16 @@ limitations under the License.
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import {
KeysBackupRequest,
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
OlmMachine,
SignatureUploadRequest,
} from "@matrix-org/matrix-sdk-crypto-js";
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-js";
import { Mocked } from "jest-mock";
import MockHttpBackend from "matrix-mock-request";
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../src/rust-crypto";
import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import { IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
import { mkEvent } from "../../test-utils/test-utils";
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "../../../src/@types/crypto";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -58,7 +53,7 @@ describe("RustCrypto", () => {
});
});
describe("to-device messages", () => {
describe("call preprocess methods", () => {
let rustCrypto: RustCrypto;
beforeEach(async () => {
@@ -103,8 +98,8 @@ describe("RustCrypto", () => {
/** the RustCrypto implementation under test */
let rustCrypto: RustCrypto;
/** A mock http backend which rustCrypto is connected to */
let httpBackend: MockHttpBackend;
/** A mock OutgoingRequestProcessor which rustCrypto is connected to */
let outgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
/** a mocked-up OlmMachine which rustCrypto is connected to */
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
@@ -113,28 +108,25 @@ describe("RustCrypto", () => {
* the front of the queue, until it is empty. */
let outgoingRequestQueue: Array<Array<any>>;
/** wait for a call to olmMachine.markRequestAsSent */
function awaitCallToMarkAsSent(): Promise<void> {
return new Promise((resolve, _reject) => {
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
resolve(undefined);
/** wait for a call to outgoingRequestProcessor.makeOutgoingRequest.
*
* The promise resolves to a callback: the makeOutgoingRequest call will not complete until the returned
* callback is called.
*/
function awaitCallToMakeOutgoingRequest(): Promise<() => void> {
return new Promise<() => void>((resolveCalledPromise, _reject) => {
outgoingRequestProcessor.makeOutgoingRequest.mockImplementationOnce(async () => {
const completePromise = new Promise<void>((resolveCompletePromise, _reject) => {
resolveCalledPromise(resolveCompletePromise);
});
return completePromise;
});
});
}
beforeEach(async () => {
httpBackend = new MockHttpBackend();
await RustSdkCryptoJs.initAsync();
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
baseUrl: "https://example.com",
prefix: "/_matrix",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
onlyData: true,
});
// for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that
// returns objects from outgoingRequestQueue
outgoingRequestQueue = [];
@@ -142,91 +134,55 @@ describe("RustCrypto", () => {
outgoingRequests: jest.fn().mockImplementation(() => {
return Promise.resolve(outgoingRequestQueue.shift() ?? []);
}),
markRequestAsSent: jest.fn(),
close: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(olmMachine, httpApi, TEST_USER, TEST_DEVICE_ID);
outgoingRequestProcessor = {
makeOutgoingRequest: jest.fn(),
} as unknown as Mocked<OutgoingRequestProcessor>;
rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi<any>, TEST_USER, TEST_DEVICE_ID);
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
});
it("should poll for outgoing messages", () => {
it("should poll for outgoing messages and send them", async () => {
const testReq = new KeysQueryRequest("1234", "{}");
outgoingRequestQueue.push([testReq]);
const makeRequestPromise = awaitCallToMakeOutgoingRequest();
rustCrypto.onSyncCompleted({});
await makeRequestPromise;
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
});
/* simple requests that map directly to the request body */
const tests: Array<[any, "POST" | "PUT", string]> = [
[KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
[KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
[KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
[SignatureUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/signatures/upload"],
[KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
];
for (const [RequestClass, expectedMethod, expectedPath] of tests) {
it(`should handle ${RequestClass.name}s`, async () => {
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RequestClass("1234", testBody);
outgoingRequestQueue.push([outgoingRequest]);
const testResponse = '{ "result": 1 }';
httpBackend
.when(expectedMethod, "/_matrix")
.check((req) => {
expect(req.path).toEqual(expectedPath);
expect(req.rawData).toEqual(testBody);
expect(req.headers["Accept"]).toEqual("application/json");
expect(req.headers["Content-Type"]).toEqual("application/json");
})
.respond(200, testResponse, true);
rustCrypto.onSyncCompleted({});
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
const markSentCallPromise = awaitCallToMarkAsSent();
await httpBackend.flushAllExpected();
await markSentCallPromise;
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
httpBackend.verifyNoOutstandingRequests();
});
}
it("does not explode with unknown requests", async () => {
const outgoingRequest = { id: "5678", type: 987 };
outgoingRequestQueue.push([outgoingRequest]);
rustCrypto.onSyncCompleted({});
await awaitCallToMarkAsSent();
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
expect(outgoingRequestProcessor.makeOutgoingRequest).toHaveBeenCalledWith(testReq);
});
it("stops looping when stop() is called", async () => {
const testResponse = '{ "result": 1 }';
for (let i = 0; i < 5; i++) {
outgoingRequestQueue.push([new KeysQueryRequest("1234", "{}")]);
httpBackend.when("POST", "/_matrix").respond(200, testResponse, true);
}
let makeRequestPromise = awaitCallToMakeOutgoingRequest();
rustCrypto.onSyncCompleted({});
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeTruthy();
// go a couple of times round the loop
await httpBackend.flush("/_matrix", 1);
await awaitCallToMarkAsSent();
let resolveMakeRequest = await makeRequestPromise;
makeRequestPromise = awaitCallToMakeOutgoingRequest();
resolveMakeRequest();
await httpBackend.flush("/_matrix", 1);
await awaitCallToMarkAsSent();
resolveMakeRequest = await makeRequestPromise;
makeRequestPromise = awaitCallToMakeOutgoingRequest();
resolveMakeRequest();
// a second sync while this is going on shouldn't make any difference
rustCrypto.onSyncCompleted({});
await httpBackend.flush("/_matrix", 1);
await awaitCallToMarkAsSent();
resolveMakeRequest = await makeRequestPromise;
outgoingRequestProcessor.makeOutgoingRequest.mockReset();
resolveMakeRequest();
// now stop...
rustCrypto.stop();
@@ -238,11 +194,40 @@ describe("RustCrypto", () => {
setTimeout(resolve, 100);
});
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy();
httpBackend.verifyNoOutstandingRequests();
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
expect(olmMachine.outgoingRequests).not.toHaveBeenCalled();
// we sent three, so there should be 2 left
expect(outgoingRequestQueue.length).toEqual(2);
});
});
describe(".getEventEncryptionInfo", () => {
let rustCrypto: RustCrypto;
beforeEach(async () => {
const mockHttpApi = {} as MatrixClient["http"];
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
});
it("should handle unencrypted events", () => {
const event = mkEvent({ event: true, type: "m.room.message", content: { body: "xyz" } });
const res = rustCrypto.getEventEncryptionInfo(event);
expect(res.encrypted).toBeFalsy();
});
it("should handle encrypted events", async () => {
const event = mkEvent({ event: true, type: "m.room.encrypted", content: { algorithm: "fake_alg" } });
const mockCryptoBackend = {
decryptEvent: () =>
({
senderCurve25519Key: "1234",
} as IEventDecryptionResult),
} as unknown as CryptoBackend;
await event.attemptDecryption(mockCryptoBackend);
const res = rustCrypto.getEventEncryptionInfo(event);
expect(res.encrypted).toBeTruthy();
});
});
});
+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());
});
});
});
+138
View File
@@ -1,3 +1,19 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as utils from "../../src/utils";
import {
alphabetPad,
@@ -8,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";
@@ -129,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);
},
};
@@ -587,4 +608,121 @@ describe("utils", function () {
expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy();
});
});
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);
});
it("resolves with the provided value", async () => {
const expected = Symbol("hi");
const result = await utils.sleep(0, expected);
expect(result).toBe(expected);
});
});
describe("immediate", () => {
it("resolves", async () => {
await utils.immediate();
});
});
});
+145 -4
View File
@@ -40,7 +40,9 @@ import {
MockMediaStreamTrack,
installWebRTCMocks,
MockRTCPeerConnection,
MockRTCRtpTransceiver,
SCREENSHARE_STREAM_ID,
MockRTCRtpSender,
} from "../../test-utils/webrtc";
import { CallFeed } from "../../../src/webrtc/callFeed";
import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
@@ -429,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);
@@ -536,8 +590,15 @@ describe("Call", function () {
it("if local video", async () => {
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
// since this is testing for the presence of a local sender, we need to add a transciever
// rather than just a source track
const mockTrack = new MockMediaStreamTrack("track_id", "video");
const mockTransceiver = new MockRTCRtpTransceiver(call.peerConn as unknown as MockRTCPeerConnection);
mockTransceiver.sender = new MockRTCRtpSender(mockTrack) as unknown as RTCRtpSender;
(call as any).transceivers.set("m.usermedia:video", mockTransceiver);
(call as any).pushNewLocalFeed(
new MockMediaStream("remote_stream1", [new MockMediaStreamTrack("track_id", "video")]),
new MockMediaStream("remote_stream1", [mockTrack]),
SDPStreamMetadataPurpose.Usermedia,
false,
);
@@ -703,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);
@@ -829,6 +901,55 @@ describe("Call", function () {
await startVideoCall(client, call);
});
afterEach(() => {
jest.useRealTimers();
});
it("should not remove video sender on video mute", async () => {
await call.setLocalVideoMuted(true);
expect((call as any).hasUserMediaVideoSender).toBe(true);
});
it("should release camera after short delay on video mute", async () => {
jest.useFakeTimers();
await call.setLocalVideoMuted(true);
jest.advanceTimersByTime(500);
expect(call.hasLocalUserMediaVideoTrack).toBe(false);
});
it("should re-request video feed on video unmute if it doesn't have one", async () => {
jest.useFakeTimers();
const mockGetUserMediaStream = jest
.fn()
.mockReturnValue(client.client.getMediaHandler().getUserMediaStream(true, true));
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;
await call.setLocalVideoMuted(true);
jest.advanceTimersByTime(500);
await call.setLocalVideoMuted(false);
expect(mockGetUserMediaStream).toHaveBeenCalled();
});
it("should not release camera on fast mute and unmute", async () => {
const mockGetUserMediaStream = jest.fn();
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;
await call.setLocalVideoMuted(true);
await call.setLocalVideoMuted(false);
expect(mockGetUserMediaStream).not.toHaveBeenCalled();
expect(call.hasLocalUserMediaVideoTrack).toBe(true);
});
describe("sending sdp_stream_metadata_changed events", () => {
it("should send sdp_stream_metadata_changed when muting audio", async () => {
await call.setMicrophoneMuted(true);
@@ -1385,7 +1506,7 @@ describe("Call", function () {
}),
);
// @ts-ignore Mock
expect(call.terminate).toHaveBeenCalledWith(CallParty.Local, CallErrorCode.Transfered, true);
expect(call.terminate).toHaveBeenCalledWith(CallParty.Local, CallErrorCode.Transferred, true);
});
});
@@ -1521,7 +1642,7 @@ describe("Call", function () {
hasAdvancedBy += advanceBy;
expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
expect(lengthChangedListener).toBeCalledWith(hasAdvancedBy);
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy, call);
}
});
@@ -1551,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);
};
});

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