Compare commits

...

777 Commits

Author SHA1 Message Date
RiotRobot 3702ac56f4 v21.2.0 2022-11-22 11:24:19 +00:00
RiotRobot af4811b327 Prepare changelog for v21.2.0 2022-11-22 11:24:19 +00:00
RiotRobot d4601d9910 v21.2.0-rc.1 2022-11-15 17:41:32 +00:00
RiotRobot 2ced5e1aa4 Prepare changelog for v21.2.0-rc.1 2022-11-15 17:41:32 +00:00
David Baker 45e19e51c1 Merge pull request #2880 from matrix-org/dbkr/revert_to_connecting
Make calls go back to 'connecting' state when media lost
2022-11-15 17:36:50 +00:00
David Baker 3f1c3392d4 Make calls go back to 'connecting' state when media lost
This is a change in how the state machine works: technically it's
a breaking change. Calls will now now go back into the connecting
state if the media connection is lost (they'll try to re-establish
the connection).
2022-11-15 16:10:45 +00:00
Richard van der Hoff f86f67f5a5 Improve logs for decrypting events (#2879)
Some attempts to make debugging UISIs a bit easier
2022-11-15 15:11:13 +00:00
Mahdi Bagvand 4dcf54f448 fix registration add phone number not working (#2876)
Co-authored-by: Michael Weimann <michaelw@matrix.org>
2022-11-15 10:42:37 +01:00
Germain d43e664594 Add ability to send unthreaded receipt (#2878) 2022-11-14 16:14:07 +00:00
Michael Telatynski 0e322848f9 Add way to abort search requests (#2877) 2022-11-14 16:11:53 +00:00
Šimon Brandner b454318684 Use an underride rule for Element Call notifications (#2873) 2022-11-14 11:42:51 +01:00
Richard van der Hoff 692f1d49b9 Deprecate/remove some get/set methods on Crypto (#2874)
* Deprecate Crypto.{get,set}GlobalBlacklistUnverifiedDevices

... in favour of just exposing the properties.

* Remove Crypto.{get,set}GlobalErrorOnUnknownDevices

... in favour of exposing the property.

These methods are UNSTABLE so we can safely remove them, right?
2022-11-14 10:03:12 +00:00
Richard van der Hoff b40cf75c9d Remove Crypto.start() (#2871)
This isn't really needed, and its semantics are poorly defined. (Contrary to
the comment, it dos *not* set background processes running).
2022-11-14 10:01:05 +00:00
kegsay ba6a001d67 Merge pull request #2834 from matrix-org/kegan/custom-room-subs
sliding sync: add custom room subscriptions support
2022-11-14 09:19:40 +00:00
kegsay d0c71ec516 Merge branch 'develop' into kegan/custom-room-subs 2022-11-14 09:11:37 +00:00
Richard van der Hoff 67f343d6f0 Switch to typedoc for documentation (#2869)
This seems to give *much* better results than jsdoc, and means that we can
start to get rid of all the duplicated type information.
2022-11-11 11:38:04 +00:00
Germain a7f0ba97cd Fixes unwanted highlight notifications with encrypted threads (#2862) 2022-11-11 09:27:16 +00:00
Michael Telatynski 54d11e1745 Upload docs artifact for PRs (#2868) 2022-11-10 18:01:34 +00:00
David Baker 14744fd4dc Merge pull request #2865 from matrix-org/dbkr/log_call_id
Log the call ID when receiving a call
2022-11-09 16:03:51 +00:00
David Baker 9b0919350c Log the call ID when receiving a call 2022-11-09 15:47:37 +00:00
David Baker b53ad2c081 Merge pull request #2864 from matrix-org/dbkr/log_on_hangup
Add logging saying why we hung up calls
2022-11-09 15:42:11 +00:00
David Baker 6222d238e4 Add logging saying why we hung up calls 2022-11-09 15:20:10 +00:00
Michael Telatynski c6ee258789 Remove patch-package and update matrix-events-sdk (#2863) 2022-11-08 17:01:38 +00:00
László Várady a584324a0d webrtc: add advanced audio settings (#2434) 2022-11-08 16:02:31 +00:00
renovate[bot] 059b07cfa0 Lock file maintenance (#2857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-08 14:40:33 +00:00
RiotRobot b628cabe58 Resetting package fields for development 2022-11-08 14:36:10 +00:00
RiotRobot b7d925f5ec Merge branch 'master' into develop
# Conflicts:
#	package.json
2022-11-08 14:36:02 +00:00
RiotRobot 47b729f085 v21.1.0 2022-11-08 14:31:37 +00:00
RiotRobot 05d980608a Prepare changelog for v21.1.0 2022-11-08 14:31:37 +00:00
Michael Telatynski 1c901e3137 Fix Node 19 compatibility and run CI against it (#2842) 2022-11-08 14:01:06 +00:00
Germain bd4589fcc4 Hide pending events in thread timelines (#2843) 2022-11-08 10:04:20 +00:00
Robin 0fbd0b3685 Merge pull request #2860 from robintown/patch-package-dependency
Move patch-package out of dev dependencies
2022-11-07 15:36:20 -05:00
Eric Eastwood 1646ea05bc Extra insurance that we don't mix events in the wrong timelines - v2 (#2856)
Add checks to `addEventToTimeline` as extra insurance that we don't mix events in the wrong timelines (main timeline vs thread timeline).

Split out from https://github.com/matrix-org/matrix-js-sdk/pull/2521

This PR is a v2 of https://github.com/matrix-org/matrix-js-sdk/pull/2848 since it was reverted in https://github.com/matrix-org/matrix-js-sdk/pull/2853

Previously, we just relied on the callers to make sure they're doing the right thing and since it's easy to get it wrong, we mixed and bugs happened.

Call stacks for how events get added to a timeline:

 - `TimelineSet.addEventsToTimeline` -> `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
 - `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
 - `TimelineSet.addLiveEvent` -> `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
2022-11-07 14:24:43 -06:00
Robin Townsend 885ec1fc73 Move patch-package out of dev dependencies
patch-package is used as a postinstall hook, but since it was in devDependencies, upstream packages would not install it. Moving it to dependencies isn't ideal since it's not needed at runtime, but the patch-package approach is only a temporary workaround for https://github.com/matrix-org/matrix-events-sdk/pull/16#pullrequestreview-1166721652 anyways.
2022-11-07 15:24:04 -05:00
David Baker df2b65f111 Merge pull request #2553 from matrix-org/robertlong/group-call
Add support for group calls using MSC3401
2022-11-07 17:24:31 +00:00
David Baker f09853ccb1 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-11-07 17:04:15 +00:00
Michael Telatynski 6c543382e6 Make SonarCloud happier (#2850)
* Make SonarCloud happier

* Revert one change due to lack of strict mode upstream

* Fix typo
2022-11-07 12:16:48 +00:00
Michael Telatynski 52932f59ab Fix pagination token tracking for mixed room timelines (#2855) 2022-11-05 12:36:20 +00:00
David Baker 4f63ff21ea Merge branch 'develop' into robertlong/group-call 2022-11-04 16:05:04 +00:00
David Baker c8dc71eb69 Merge pull request #2854 from matrix-org/dbkr/gcmerge_20221104
Merge changes from develop
2022-11-04 16:04:23 +00:00
David Baker 2dda837db6 Fix strict mode errors 2022-11-04 14:44:21 +00:00
David Baker fff4cdab7c Merge remote-tracking branch 'origin/develop' into dbkr/gcmerge_20221104 2022-11-04 14:05:48 +00:00
Šimon Brandner 4f00566b9f Do not freeze state in initialiseState() (#2846) 2022-11-04 14:48:42 +01:00
Michael Telatynski c1a3b95073 Revert "Extra insurance that we don't mix events in the wrong timelines" (#2853)
This reverts commit 433b7afd71.
2022-11-04 12:21:04 +00:00
Eric Eastwood 38adbaf923 Ignore random macOS cruft (.DS_Store) (#2851) 2022-11-04 06:25:10 -05:00
Travis Ralston 9459a95134 Delete .DS_Store 2022-11-04 10:59:23 +00:00
Eric Eastwood 433b7afd71 Extra insurance that we don't mix events in the wrong timelines (#2848)
Add checks to `addEventToTimeline` as extra insurance that we don't mix events in the wrong timelines (main timeline vs thread timeline).

Split out from https://github.com/matrix-org/matrix-js-sdk/pull/2521

Previously, we just relied on the callers to make sure they're doing the right thing and since it's easy to get it wrong, we mixed and bugs happened.

Call stacks for how events get added to a timeline:

 - `TimelineSet.addEventsToTimeline` -> `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
 - `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
 - `TimelineSet.addLiveEvent` -> `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
2022-11-04 05:54:23 -05:00
Michael Telatynski 777cf1f135 Remove tsc-strict ci, it has served its purpose (#2847) 2022-11-04 09:12:07 +00:00
Robin 8235b65d71 Merge pull request #2844 from robintown/dont-remove-self
Don't remove our own member for a split second when entering a call
2022-11-03 14:38:39 -04:00
Hubert Chathi 4a33e584b0 Add unit test for device de-/rehydration (#2821) 2022-11-03 13:12:57 -04:00
Michael Telatynski 6ee185e93f Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	.github/workflows/sonarqube.yml
2022-11-03 14:13:25 +00:00
Michael Telatynski 5df9705bae Update sonarqube.yml 2022-11-03 14:12:42 +00:00
Michael Telatynski 76458d3a40 Update sonarqube.yml 2022-11-03 13:58:50 +00:00
Michael Telatynski 27bb79a29a Switch to @casualbot/jest-sonar-reporter 2022-11-03 13:47:36 +00:00
Michael Telatynski 82d942dcc5 Update sonarqube.yml 2022-11-03 13:17:48 +00:00
Michael Telatynski ce6d0e2cb1 Update sonarqube.yml 2022-11-03 12:59:16 +00:00
Michael Telatynski db49cd8d13 Make the js-sdk conform to tsc --strict (#2835)
Co-authored-by: Faye Duxovni <fayed@matrix.org>
2022-11-03 12:50:05 +00:00
Michael Telatynski 42b08eca57 Update sonarqube.yml 2022-11-03 12:49:38 +00:00
Michael Telatynski a92c148f15 Update sonarqube.yml 2022-11-03 12:23:04 +00:00
Michael Telatynski 6cd60e32dc Update sonarqube.yml 2022-11-03 12:14:16 +00:00
Michael Telatynski b6633ad4b0 Update sonarqube.yml 2022-11-03 12:06:52 +00:00
Michael Telatynski e4dd7bcc87 Update sonarcloud.yml 2022-11-03 12:01:33 +00:00
Michael Telatynski b6e97fcecb Update sonarqube.yml 2022-11-03 12:01:20 +00:00
Michael Telatynski dee2b60c3d Update sonarqube.yml 2022-11-03 11:59:54 +00:00
Michael Telatynski a07fe44565 Update matrix-org/sonarcloud-workflow-action (#2845) 2022-11-03 11:46:31 +00:00
Michael Telatynski 0a35f2e2c7 Fix queueToDevice.spec.ts test flakiness (#2841) 2022-11-03 11:45:35 +00:00
Robin Townsend 32d535c2b1 Don't remove our own member for a split second when entering a call 2022-11-02 22:43:54 -04:00
David Baker 7fb313c17c Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-11-02 10:38:05 +00:00
renovate[bot] d8f6449422 Update jest monorepo to v29.2.1 (#2837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-02 08:53:09 +00:00
renovate[bot] c043e36f50 Update all (#2836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-02 08:52:32 +00:00
renovate[bot] e6524239bd Update dependency @babel/runtime to v7.20.1 (#2838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-02 02:20:24 -04:00
renovate[bot] daed4b9dcc Update typescript-eslint monorepo to v5.42.0 (#2839)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-01 20:29:45 +00:00
Kegan Dougal 135d2da143 Maybe satisfy the strict ts checker 2022-11-01 16:35:40 +00:00
Kegan Dougal fef53be5b4 Use Map not Objects 2022-11-01 16:31:17 +00:00
David Baker 7ec726e10b Give everything that isn't web rtc back to element-web 2022-11-01 16:18:14 +00:00
David Baker 476f6f78b1 Add more access modifiers 2022-11-01 16:14:48 +00:00
David Baker cb8123dec7 Add public/private modifiers 2022-11-01 16:07:07 +00:00
David Baker 6729c7d421 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-11-01 14:38:16 +00:00
RiotRobot 99af67a963 v21.1.0-rc.1 2022-11-01 14:38:04 +00:00
RiotRobot b77f5a5598 Prepare changelog for v21.1.0-rc.1 2022-11-01 14:38:04 +00:00
Michael Telatynski 76377c7cc4 Add eslint rule unicorn/no-instanceof-array (#2833) 2022-11-01 14:24:47 +00:00
Kegan Dougal 7ddd198df8 Linting 2022-11-01 11:08:47 +00:00
Kegan Dougal 81681f4090 Add custom room subscriptions support
This is mostly useful when you need to change the subscription depending
on the room. For example, unencrypted rooms have lazy-loaded members, but
encrypted rooms do not.
2022-11-01 11:04:40 +00:00
RiotRobot 52830a2a50 Merge branch 'master' into develop
# Conflicts:
#	src/client.ts
2022-11-01 09:21:54 +00:00
RiotRobot 81c3668cb6 v21.0.1 2022-11-01 09:16:11 +00:00
RiotRobot 8f40dc6304 Prepare changelog for v21.0.1 2022-11-01 09:16:11 +00:00
Michael Telatynski fcdd8c93f4 [Backport staging] Catch server versions API call exception when starting the client (#2832)
Co-authored-by: Germain <germains@element.io>
2022-11-01 09:01:52 +00:00
ElementRobot 7d7803380c [Backport staging] Fix default behavior of Room.getBlacklistUnverifiedDevices (#2831)
Co-authored-by: Faye Duxovni <fayed@matrix.org>
2022-11-01 08:47:47 +00:00
Faye Duxovni 9fa6616052 Fix default behavior of Room.getBlacklistUnverifiedDevices (#2830) 2022-10-31 18:21:27 +00:00
Robin 94072a096d Merge pull request #2826 from robintown/init-leave-race
Resolve races between `initLocalCallFeed` and `leave`
2022-10-31 13:46:30 -04:00
Germain 1f3ae4bde2 Catch server versions API call exception when starting the client (#2828) 2022-10-31 17:44:52 +00:00
ElementRobot 545a74364d [Backport staging] Fix authedRequest including Authorization: Bearer undefined for password resets (#2829)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-31 17:14:06 +00:00
Michael Telatynski 646b3a69fe Fix authedRequest including Authorization: Bearer undefined for password resets (#2822) 2022-10-31 17:08:35 +00:00
ElementRobot db33f396b8 [Backport staging] Fix JSDoc (#2827)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-31 17:00:45 +00:00
Michael Telatynski 6c475d9b54 Fix JSDoc (#2825) 2022-10-31 16:50:15 +00:00
Robin Townsend b9cccf9109 Resolve races between initLocalCallFeed and leave
Unfortunately there are still other methods that could race with leave and result in broken group call state, such as enter and terminate. For the future, should consider writing a more careful specification of how the whole group call state machine is meant to work.
2022-10-31 12:09:06 -04:00
David Baker f0d4ef7f99 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-10-31 15:36:30 +00:00
Janne Mareike Koschinski 068fbb7660 Loading threads with server-side assistance (#2735)
* Fix bug where undefined vs null in pagination tokens wasn't correctly handled
* Fix bug where thread list results were sorted incorrectly
* Allow removing the relationship of an event to a thread
* Implement feature detection for new threads MSCs and specs
* Prefix dir parameter for threads pagination if necessary
* Make threads conform to the same timeline APIs as any other timeline
* Extract thread timeline loading out of thread class
* fix thread roots not being updated correctly
* fix jumping to events by link
* implement new thread timeline loading
* Fix fetchRoomEvent incorrect return type

Co-authored-by: Germain <germains@element.io>
Co-authored-by: Germain <germain@souquet.com>
2022-10-28 13:48:14 +02:00
Robin 849e3d67c2 Merge pull request #2815 from robintown/keepalive-leave
Let leave requests outlive the window
2022-10-27 08:35:30 -04:00
Robin Townsend 4c6e1e5c21 Replace the keepAlive flag with request options 2022-10-27 08:19:41 -04:00
David Baker 9ff6b357fc Merge branch 'develop' into robertlong/group-call 2022-10-27 09:56:36 +01:00
David Baker 77ef8558bd Merge pull request #2816 from matrix-org/dbkr/groupcall_more_strict
A few more strict mode fixes
2022-10-27 09:55:17 +01:00
David Baker d979302e9b A few more strict mode fixes 2022-10-27 09:37:41 +01:00
Robin Townsend dbdaa1540a Let leave requests outlive the window 2022-10-26 17:50:20 -04:00
Michael Telatynski b44787192d Replace instanceof Array with Array.isArray (#2812) 2022-10-26 17:59:16 +01:00
Germain 6f2390a765 Switch ESLint warnings to be errors instead (#2814) 2022-10-26 16:25:40 +00:00
Germain dddc0aeccb Emit UnreadNotification event on notifications reset (#2804) 2022-10-26 14:23:54 +01:00
kegsay 9f6b42d3ae Merge pull request #2801 from matrix-org/kegan/ss-api-changes
Sliding sync: add include_old_rooms; remove is_tombstoned
2022-10-26 14:03:55 +01:00
David Baker 13c751c060 Merge pull request #2808 from matrix-org/dbkr/groupcall_more_strict
More TS strict mode fixes
2022-10-26 13:44:00 +01:00
Kegan Dougal 2e56c34df0 Merge branch 'develop' into kegan/ss-api-changes 2022-10-26 13:03:21 +01:00
David Baker 87115d181d Don't commit the strict mode flag 2022-10-26 12:34:18 +01:00
David Baker 5679c86ca6 More TS strict mode fixes 2022-10-26 12:33:06 +01:00
David Baker 4cd50e4871 Merge pull request #2807 from matrix-org/dbkr/gcmerge_26oct22
Merge changes from develop again
2022-10-26 12:08:28 +01:00
David Baker 0f1012278a Fix types 2022-10-26 12:01:53 +01:00
David Baker 0d211dfbad Clean up group call tests (#2806) 2022-10-26 11:57:16 +01:00
David Baker 384116c8f5 Merge remote-tracking branch 'origin/develop' into dbkr/gcmerge_26oct22 2022-10-26 11:56:41 +01:00
David Baker c374ba2367 TS strict mode compliance in the call / groupcall code (#2805)
* TS strict mode compliance in the call / groupcall code

* Also the test

* Fix initOpponentCrypto

to not panic if it doesn't actually need to init crypto
2022-10-26 11:45:03 +01:00
Michael Telatynski 9f2f08dfd3 Fix more typescript --strict violations (#2795)
* Stash tsc fixes

* Iterate

* Iterate

* Iterate

* Fix tests

* Iterate

* Iterate

* Iterate

* Iterate

* Add tests
2022-10-25 18:31:40 +01:00
RiotRobot 4b3e6939d6 Resetting package fields for development 2022-10-25 17:05:31 +01:00
RiotRobot f2ae3bc8ef Merge branch 'master' into develop 2022-10-25 17:05:28 +01:00
RiotRobot 1842004db2 v21.0.0 2022-10-25 17:04:02 +01:00
RiotRobot c8c7af0ae2 Prepare changelog for v21.0.0 2022-10-25 17:04:02 +01:00
Kegan Dougal 11cc30f345 Remove debug logging 2022-10-25 13:47:16 +01:00
Kegan Dougal 24a9562b07 bugfix: allow subtly different DELETE/INSERT semantics
In sliding sync, with an empty list, it is possible for the proxy
to send back DELETE 0, INSERT 0 !room which has the net result of
`[!room]`. Previously, the JS SDK would not handle this correctly.
Now it does. With tests.
2022-10-25 13:07:53 +01:00
Kegan Dougal 8f10c0d921 Sliding sync: add include_old_rooms; remove is_tombstoned 2022-10-25 10:45:38 +01:00
David Baker 450ff00c3e Merge pull request #2800 from matrix-org/dbkr/gcmerge_oct22_3
Merge develop into group-call branch
2022-10-24 19:03:06 +01:00
David Baker b4ab7fc0b3 Merge branch 'robertlong/group-call' into dbkr/gcmerge_oct22_3 2022-10-24 18:53:54 +01:00
Robin 35f697a04b Merge pull request #2797 from robintown/matryoshka-events
Add event and message capabilities to RoomWidgetClient
2022-10-24 13:50:09 -04:00
David Baker 193d8a429a Merge remote-tracking branch 'origin/develop' into dbkr/gcmerge_oct22_3 2022-10-24 18:38:46 +01:00
Robin Townsend 8cd5aac128 Add event and message capabilities to RoomWidgetClient 2022-10-24 12:18:02 -04:00
RiotRobot e7ce1fb9e8 v21.0.0-rc.2 2022-10-24 16:19:47 +01:00
RiotRobot 7772f855e6 Prepare changelog for v21.0.0-rc.2 2022-10-24 16:19:46 +01:00
Michael Telatynski 9bdeea0a8d Fix incorrect prevEv being sent in ClientEvent.AccountData events (#2794) 2022-10-24 15:30:55 +01:00
Michael Telatynski ade2e81d3d Revert "Sliding sync: add include_old_rooms; remove is_tombstoned" (#2796) 2022-10-24 14:16:02 +01:00
kegsay 2fe434f3ae Merge pull request #2785 from matrix-org/kegan/ss-api-changes
Sliding sync: add include_old_rooms; remove is_tombstoned
2022-10-24 13:23:51 +01:00
David Baker eddd0cafe8 Add throwOnFail to groupCall.setScreensharingEnabled (#2787)
* Add throwOnFail to groupCall.setScreensharingEnabled

For https://github.com/vector-im/element-call/pull/652

* Update mediaHandler.ts
2022-10-24 10:20:04 +01:00
ElementRobot 4ccc52da8e [Backport staging] Improve crypto init code and allow easier shimming (#2792)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-24 10:12:42 +01:00
Michael Telatynski 3a6561af36 Improve crypto init code and allow easier shimming (#2791) 2022-10-24 09:40:18 +01:00
renovate[bot] 403286cb81 Lock file maintenance (#2790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-24 09:07:13 +01:00
Kegan Dougal 219eab9139 Sliding sync: add include_old_rooms; remove is_tombstoned 2022-10-21 17:16:55 +01:00
Šimon Brandner a12e6185f9 Update call notification push rule to match MSC3914 (#2781) 2022-10-21 15:17:34 +00:00
Michael Telatynski d9eac57e9c Add room_type field to /publicRooms response (#2784) 2022-10-21 16:09:34 +01:00
renovate[bot] 9a9009d838 Update dependency jest-mock to v29 (#2775)
* Update dependency jest-mock to v29

* Update imports

* Strict fixes

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-21 14:22:50 +00:00
Michael Telatynski 6f729ad7fd Switch /keys/signatures/upload to v3 prefix (#2782) 2022-10-21 14:31:28 +01:00
Janne Mareike Koschinski cd33bafa04 fix build error caused by wrong ts-strict improvements (#2783)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-21 13:29:05 +00:00
Michael Telatynski 867a0ca7ee Apply more strict typescript around the codebase (#2778)
* Apply more strict typescript around the codebase

* Fix tests

* Revert strict mode commit

* Iterate strict

* Iterate

* Iterate strict

* Iterate

* Fix tests

* Iterate

* Iterate strict

* Add tests

* Iterate

* Iterate

* Fix tests

* Fix tests

* Strict types be strict

* Fix types

* detectOpenHandles

* Strict

* Fix client not stopping

* Add sync peeking tests

* Make test happier

* More strict

* Iterate

* Stabilise

* Moar strictness

* Improve coverage

* Fix types

* Fix types

* Improve types further

* Fix types

* Improve typing of NamespacedValue

* Fix types
2022-10-21 11:44:40 +01:00
Richard van der Hoff fdbbd9bca4 Merge pull request #2777 from matrix-org/rav/debug_to_device
An attempt to debug vector-im/element-web#23548: let's see if we can figure out where the to-device messages are getting lost.
2022-10-20 12:04:08 +01:00
Germain bf1137fc58 Fix to keep locally computed thread notifications (#2768) 2022-10-20 08:57:45 +01:00
David Baker 5a0787349d Fix connectivity regressions (#2780)
* Fix connectivity regressions

Switches back to addTrack, digging the transceivers out manually
to re-use, because the only way to group tracks into streams re-using
trasceivers from the offer is to use setStreams which FF doesn't
implement.

* Remove comments
2022-10-19 18:03:12 +01:00
David Baker c57c8978cf Fix screenshare failing after several attempts (#2771)
* Fix screenshare failing after several attempts

Re-use any existing transceivers when screen sharing. This prevents
transceivers accumulating and making the SDP too big: see linked bug.

This also switches from `addTrack()` to `addTransceiver ()` which is
not that large of a change, other than having to explicitly find the
transceivers after an offer has arrived rather than just adding tracks
and letting WebRTC take care of it.

Fixes https://github.com/vector-im/element-call/issues/625

* Fix tests

* Unused import

* Use a map instead of an array

* Add comment

* more comment

* Remove commented code

* Remove unintentional debugging

* Add test for screenshare transceiver re-use

* Type alias for transceiver map
2022-10-19 16:00:54 +01:00
renovate[bot] 508bb5841c Update all (#2774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-19 12:59:56 +00:00
Richard van der Hoff 35227e3a75 Merge pull request #2776 from matrix-org/rav/fix-readme 2022-10-19 11:47:10 +01:00
Richard van der Hoff 620a8d9c7f Add debugging for unsent to-device messages 2022-10-19 11:23:52 +01:00
renovate[bot] 17e16b9b1a Update jest monorepo to v29.2.0 (#2773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-19 10:55:47 +01:00
renovate[bot] 671dedca1c Update typescript-eslint monorepo to v5.40.1 (#2772)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-19 10:54:36 +01:00
Hugh Nimmo-Smith 2464a691ef Support sign in + E2EE set up using QR code implementing MSC3886, MSC3903 and MSC3906 (#2747)
* Clean implementation of MSC3886 and MSC3903

* Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better

* Start of some unit tests

* Make AES work on Node.js as well as browser

* Tests for ECDH/X25519

* stric mode linting

* Fix incorrect test

* Refactor full rendezvous logic out of react-sdk into js-sdk

* Use correct unstable import

* Pass fetch around

* Make correct usage of fetch in tests

* fix: you can't call fetch when it's not on window

* Use class names to make it clearer that these are unstable MSC implementations

* Linting

* Clean implementation of MSC3886 and MSC3903

* Refactor to use object initialiser instead of lots of args + handle non-compliant fetch better

* Start of some unit tests

* Make AES work on Node.js as well as browser

* Tests for ECDH/X25519

* stric mode linting

* Fix incorrect test

* Refactor full rendezvous logic out of react-sdk into js-sdk

* Use correct unstable import

* Pass fetch around

* Make correct usage of fetch in tests

* fix: you can't call fetch when it's not on window

* Use class names to make it clearer that these are unstable MSC implementations

* Linting

* Reduce log noise

* Tidy up interface a bit

* Additional test for transport layer

* Linting

* Refactor dummy transport to be re-usable

* Remove redundant condition

* Handle more error cases

* Initial tests for MSC3906

* Reduce scope of PR to only cover generating a code on existing device

* Strict linting

* Additional test cases

* Lint

* additional test cases and remove some code smells

* More test cases

* Strict lint

* Strict lint

* Test case

* Refactor to handle UIA

* Unstable prefixes

* Lint

* Missed due to lack of strict...

* Test server capabilities using Feature

* Remove redundant assignment

* Refactor ro resuse generateDecimal from SAS

* Update src/rendezvous/transports/simpleHttpTransport.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/rendezvous/transports/simpleHttpTransport.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/rendezvous/channels/ecdhV1.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/rendezvous/transports/simpleHttpTransport.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Rename files to titlecase

* Visibility modifiers

* Resolve public mutability

* Refactor logic to reduce duplication

* Refactor to have better defined data types throughout

* Rebase and remove Node.js crypto

* Wipe AES key out after use

* Add typing for MSC3906 layer

* Strict lint

* Fix double connect detection

* Remove unintended debug statement

* Return types

* Use generics

* Make type of MSC3903ECDHPayload explicit

* Use unstable prefix for RendezvousChannelAlgorithm

* Fix

* Extra unstable type

* Test types

Co-authored-by: Travis Ralston <travisr@matrix.org>
Co-authored-by: Kerry <kerrya@element.io>
2022-10-19 09:30:15 +00:00
Richard van der Hoff d548b04d06 README.md: fix jsdoc viewer incantation
SimpleHTTPServer was python 2.
2022-10-19 10:19:23 +01:00
Richard van der Hoff 7ffdf17213 Merge pull request #2767 from matrix-org/rav/fix_comment 2022-10-19 10:13:18 +01:00
Valere 1c3dd0e51e Encryption should not hinder verification (#2734)
Co-authored-by: Faye Duxovni <fayed@matrix.org>
2022-10-18 15:56:34 -04:00
ElementRobot 63f4bf571e [Backport staging] Fix POST data not being passed for registerWithIdentityServer (#2770)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-18 15:03:40 +00:00
Michael Telatynski 0231d40277 Fix POST data not being passed for registerWithIdentityServer (#2769) 2022-10-18 15:58:17 +01:00
RiotRobot fc1b03c0bf v21.0.0-rc.1 2022-10-18 12:49:22 +01:00
RiotRobot e592f60240 Prepare changelog for v21.0.0-rc.1 2022-10-18 12:49:21 +01:00
Richard van der Hoff b1e9f39d65 Remove incorrect comment
As with most of the crypto functionality, `setCryptoTrustCrossSignedDevices`
cannot be called until after `initCrypto`.
2022-10-17 22:58:04 +01:00
David Baker dfe535bc07 More debugging for multiple group calls (#2766) 2022-10-17 20:14:44 +01:00
Michael Telatynski 30570bcce6 Remove node-specific crypto bits, use Node 16's WebCrypto (#2762) 2022-10-17 17:54:54 +01:00
Michael Telatynski 6af3b114e1 Fix IdentityPrefix.V2 containing spurious /api (#2761) 2022-10-17 14:32:40 +01:00
Michael Telatynski 041f9951c5 Remove deprecated m.room.aliases references (#2759) 2022-10-17 10:58:48 +01:00
Germain be11fa6b5a Fix notification type when resetting threads notifications (#2757) 2022-10-17 09:27:28 +01:00
Stanislav Demydiuk 6245661cd7 Export types for MatrixEvent and Room emitted events, and make event handler map types stricter (#2750) 2022-10-14 21:18:00 -04:00
Michael Telatynski 12a4d2a749 Make more of the code conform to Strict TSC (#2756) 2022-10-14 15:57:08 +01:00
kegsay f70f6db926 Merge pull request #2753 from matrix-org/kegan/http-code-on-non-json
Always send back an httpStatus property if one is known
2022-10-14 10:31:12 +01:00
Kegan Dougal 500601ea85 More tests to satisfy sonarcloud 2022-10-14 10:23:18 +01:00
Robin Townsend 3c33c422e6 Merge branch 'develop' into robertlong/group-call 2022-10-13 20:21:51 -04:00
Robin d521f97411 Merge pull request #2754 from robintown/unblock-mute
Don't block muting/unmuting on network requests
2022-10-13 19:54:57 -04:00
Robin Townsend c0a5299704 Don't block muting/unmuting on network requests
(PTT mode will still block on them, as expected)
2022-10-13 11:56:46 -04:00
Kegan Dougal e32dfccbd9 Additional tests for 'url' property to satisfy code coverage 2022-10-13 15:39:23 +01:00
Kegan Dougal ed78737768 Fix types on utils UTs 2022-10-13 15:19:26 +01:00
Kegan Dougal ac561b743b Linting 2022-10-13 15:04:01 +01:00
Kegan Dougal e8be7af751 err already sets httpStatus now 2022-10-13 15:02:52 +01:00
Kegan Dougal 400b457edf Fix broken tests 2022-10-13 14:59:15 +01:00
Kegan Dougal 5ed4e9f535 Always send back an httpStatus property if one is known
Previously, non-JSON responses would be missing the `httpStatus`
property, which was different to how `request()` used to work.

Ensure we always send this property, even for non-JSON responses.
2022-10-13 14:53:03 +01:00
Germain c81d759334 Emit events when setting unread notifications (#2748)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-10-13 14:09:33 +01:00
kegsay 50dd79c595 Check for AbortError, not any generic connection error, to avoid tightlooping (#2752) 2022-10-13 12:18:31 +00:00
kegsay 2eb0afbad5 Merge pull request #2605 from matrix-org/kegan/sync-v3
Automatically reconnect sessions when sliding sync expires them
2022-10-13 12:53:32 +01:00
Kegan Dougal 007ca97741 Linting 2022-10-13 11:21:27 +01:00
Kegan Dougal f81e53c908 Rejig timeout function to cancel the timer when listenUntil returns 2022-10-13 11:17:44 +01:00
Kegan Dougal 89d743ac48 New style abort 2022-10-13 11:07:34 +01:00
kegsay fd61a49157 Merge branch 'develop' into kegan/sync-v3 2022-10-13 10:56:36 +01:00
Kegan Dougal bb3d51652d Consolidate error handling retry logic 2022-10-13 10:52:40 +01:00
Michael Telatynski bbece73346 Improve MatrixError message (#2749) 2022-10-13 09:07:15 +01:00
Travis Ralston cc025ea458 Fix more key backup paths being unstable (#2746) 2022-10-12 18:10:47 +00:00
renovate[bot] d06a3a47c3 Lock file maintenance (#2743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-12 18:07:58 +00:00
Michael Telatynski 34c5598a3f Modernize http-api - move from browser-request to fetch (#2719) 2022-10-12 18:59:04 +01:00
Kegan Dougal 41a973a3c6 Merge branch 'develop' into kegan/sync-v3 2022-10-12 17:22:52 +01:00
Dominik Henneke 913660c818 Correct the dir parameter of MSC3715 (#2745) 2022-10-12 16:56:50 +02:00
RiotRobot 8eed354e17 Resetting package fields for development 2022-10-11 13:43:31 +01:00
RiotRobot 7e12b62b7c Merge branch 'master' into develop 2022-10-11 13:43:28 +01:00
RiotRobot aa5a34948a v20.1.0 2022-10-11 13:41:59 +01:00
RiotRobot 6ba35e9fbc Prepare changelog for v20.1.0 2022-10-11 13:41:58 +01:00
Michael Telatynski fe2c35092e Upgrade to Olm 3.2.13 which has been repackaged to support Node 18 (#2744) 2022-10-10 13:34:33 +01:00
Šimon Brandner e37aab2967 Fix power_level_content_override type (#2741) 2022-10-07 16:40:40 +02:00
Germain 62007ec673 Fix sync init when thread unread notif is not supported (#2739)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-07 10:38:53 +00:00
Travis Ralston 029280b9d9 Update hidden characters regex (#2738) 2022-10-06 22:41:57 -06:00
Šimon Brandner 6e5326f9c8 Add custom notification handling for MSC3401 call events (#2720) 2022-10-06 16:40:30 +02:00
Kerry a1b046b5d8 test typescriptification - spec/integ (#2714)
* renamed:    spec/integ/devicelist-integ.spec.js -> spec/integ/devicelist-integ.spec.ts

* fix ts issue in devicelist-integ.spec

* renamed:    spec/integ/matrix-client-event-emitter.spec.js -> spec/integ/matrix-client-event-emitter.spec.ts

* ts issues in matrix-client-event-emitter integ

* strict fixes

* renamed:    spec/integ/matrix-client-methods.spec.js -> spec/integ/matrix-client-methods.spec.ts

* fix ts issues

* renamed:    spec/integ/matrix-client-opts.spec.js -> spec/integ/matrix-client-opts.spec.ts

* ts fixes in matrix-client-methods / matrix-client-opts

* renamed:    spec/integ/matrix-client-room-timeline.spec.js -> spec/integ/matrix-client-room-timeline.spec.ts

* most ts fixes in matrix-client-room-timeline

* remove obsoleted prev_events from mockenvents

* make xmlhttprequest ts

* strict errors in matrix-client-event-timeline spec

* strict in devicelist

* strict fixes in matrix-client-crypto.spec

* strict fixes in spec/integ/matrix-client-room-timeline

* strict issues in matrix-client-opts.specc

* strict issues in matrix-client-syncing

* strict issues in spec/integ/megolm

* strict fixes in spec/integ/matrix-client-retrying.spec

* strict fixes for spec/integ/sliding-sync

* eslint fixes

* more strict errors sneaking in from develop

* kill al httpbackends

* kill matrix-client-methods.spec httpbackend properly
2022-10-06 08:11:25 +02:00
Janne Mareike Koschinski 3a3dcfb254 Load Thread List with server-side assistance (MSC3856) (#2602)
* feature detection code for thread list api
* fix bug where createThreadsTimelineSets would sometimes return nothing
* initial implementation of thread listing msc
* tests for thread list pagination
2022-10-05 23:10:42 +02:00
RiotRobot 121250a6fb v20.1.0-rc.2 2022-10-05 13:23:42 +01:00
RiotRobot a7aa227f55 Prepare changelog for v20.1.0-rc.2 2022-10-05 13:23:42 +01:00
Germain 21a6f61b7b Add support for unread thread notifications (#2726) 2022-10-05 10:37:45 +01:00
renovate[bot] ff720e3aa3 Update all (#2732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-04 22:43:50 -06:00
renovate[bot] 2935daeb3f Update typescript-eslint monorepo to v5.39.0 (#2733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-04 22:43:38 -06:00
ElementRobot 04c1dfe43a Use the correct sender key when checking shared secret (#2730) (#2731)
(cherry picked from commit 890a840685)

Co-authored-by: Hubert Chathi <hubertc@matrix.org>
2022-10-04 11:27:40 -06:00
Hubert Chathi 890a840685 Use the correct sender key when checking shared secret (#2730) 2022-10-04 13:00:45 -04:00
Travis Ralston b1ed972867 Use stable calls to /room_keys (#2729)
* Use stable calls to `/room_keys`

Fixes https://github.com/vector-im/element-web/issues/22839

* Appease the CI
2022-10-04 10:55:16 -06:00
Kerry 2f24e90e53 Device manager - last_seen_user_agent device property (#2728)
* add last_seen_user_agent to IMyDevice type

* add ubstable value

* use unstable value in interface
2022-10-04 15:50:14 +00:00
RiotRobot 07476a0ae0 v20.1.0-rc.1 2022-10-04 14:01:35 +01:00
RiotRobot 6348704bec Prepare changelog for v20.1.0-rc.1 2022-10-04 14:01:34 +01:00
mcalinghee f84a33910c Unexpected ignored self key request when it's not shared history (#2724)
* ignore forwarded key process also if the user is not the same
2022-10-04 14:31:21 +02:00
Germain 0ccf7c50f2 Add custom error message for restricted import (#2727) 2022-10-04 09:59:20 +01:00
Šimon Brandner ead33003b7 Check for the spec version when determining private read receipt support (#2722) 2022-10-04 06:50:42 +00:00
Faye Duxovni 7d5360a00f Rename redecryption-related function arguments for clarity (#2709) 2022-10-03 19:51:30 -04:00
renovate[bot] 4b283015ba Lock file maintenance (#2721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-03 08:30:28 +01:00
Michael Telatynski 887e15aac5 Fix IDB initial migration handling causing spurious lazy loading upgrade loops (#2718) 2022-09-30 10:41:04 +00:00
RiotRobot a83d80f502 Merge branch 'master' into develop 2022-09-30 10:59:52 +01:00
RiotRobot 6166a8f7fd v20.0.2 2022-09-30 10:58:34 +01:00
RiotRobot 3efc18cfde Prepare changelog for v20.0.2 2022-09-30 10:58:33 +01:00
ElementRobot 5520aa3e2a [Backport staging] Fix issue in sync when crypto is not supported by client (#2716)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Stanislav Demydiuk <stas-demydiuk@users.noreply.github.com>
2022-09-30 10:57:04 +01:00
Michael Telatynski 5afe373446 Fix release-npm.yml dist-tag npm token passing mechanism (#2717) 2022-09-30 10:56:36 +01:00
Stanislav Demydiuk 9bb5afe5c0 Fix issue in sync when crypto is not supported by client (#2715)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-09-30 08:12:31 +00:00
Michael Telatynski f349663329 Add CI to protect against mixing src and lib imports (#2704) 2022-09-30 09:05:28 +01:00
Faye Duxovni f398e3564d Calculate IndexedDB versions automatically to reduce repeated information and possibilities for error (#2713) 2022-09-29 16:25:19 -04:00
Robin ce3b72c850 Merge pull request #2712 from robintown/merge
Merge develop
2022-09-29 08:03:36 -04:00
Robin Townsend 935517746a Merge branch 'develop' into robertlong/group-call 2022-09-28 14:13:45 -04:00
RiotRobot 91171afddd Merge branch 'master' into develop 2022-09-28 17:48:45 +01:00
RiotRobot b54c9d689a v20.0.1 2022-09-28 17:47:24 +01:00
RiotRobot 4e69d7c9ac Prepare changelog for v20.0.1 2022-09-28 17:47:23 +01:00
ElementRobot bf9f595984 [Backport staging] Fix missing return when receiving an invitation without shared history (#2711)
Co-authored-by: Faye Duxovni <fayed@element.io>
2022-09-28 16:40:58 +00:00
Faye Duxovni f410e71bfa Fix missing return when receiving an invitation without shared history (#2710) 2022-09-28 17:36:09 +01:00
RiotRobot 83fca5b57d Merge branch 'master' into develop 2022-09-28 15:46:11 +01:00
RiotRobot 90052670e7 v20.0.0 2022-09-28 15:44:47 +01:00
RiotRobot db49a1a623 Prepare changelog for v20.0.0 2022-09-28 15:44:47 +01:00
ElementRobot 45330c6418 [Backport staging] Bump IDB crypto store version (#2708)
Co-authored-by: Faye Duxovni <fayed@element.io>
2022-09-28 15:43:43 +01:00
Faye Duxovni 4ba083e6af Bump IDB crypto store version (#2705)
* Bump IDB crypto store version

* lint fix
2022-09-28 15:39:37 +01:00
Germain 0403e4bedc Fix incorrect MSC3890 unstable prefix (#2703) 2022-09-28 15:37:13 +02:00
RiotRobot 14aa7846a5 Merge branch 'master' into develop 2022-09-28 14:05:21 +01:00
RiotRobot 2d067ad957 v19.7.0 2022-09-28 14:03:53 +01:00
RiotRobot 418aa3ff6a Prepare changelog for v19.7.0 2022-09-28 14:03:52 +01:00
RiotRobot a587d7c360 Resolve multiple CVEs
CVE-2022-39249
CVE-2022-39250
CVE-2022-39251
CVE-2022-39236
2022-09-28 13:55:15 +01:00
David Baker e48d919cd4 Fix ICE restarts (#2702)
We didn't reset the 'seen end of candidates' flag when doign an ICE
restart, so we would have ignored all locally gathered candidates
on an ICE restart.
2022-09-27 17:25:04 +01:00
RiotRobot 45348a354e Resetting package fields for development 2022-09-27 16:48:03 +01:00
RiotRobot fa3339fc84 Merge branch 'master' into develop 2022-09-27 16:48:00 +01:00
RiotRobot b64a30f0ad v19.6.0 2022-09-27 16:46:53 +01:00
RiotRobot 5451f6139a Prepare changelog for v19.6.0 2022-09-27 16:46:52 +01:00
Germain b332c6c4b9 Account data can return undefined (#2701) 2022-09-27 12:47:05 +00:00
Germain 209a101be7 Add local notification settings capability (#2700) 2022-09-27 11:41:20 +01:00
Šimon Brandner ab39ee37d6 Add more MatrixCall tests (#2697)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-26 12:02:41 +02:00
Šimon Brandner af6f9d49f4 Add CallEventHandler tests (#2696)
* Add `CallEventHandler` tests

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Avoid tests hanging

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-26 12:02:19 +02:00
Michael Telatynski efbf5479d1 Remove redundant rel_branch logic (#2698) 2022-09-26 10:05:58 +01:00
Hugh Nimmo-Smith dacef048be Typings for MSC3824 (#2691) 2022-09-23 16:23:50 -06:00
Šimon Brandner a2981efac3 Add MatrixClient group call tests (#2692)
Co-authored-by: Robin <robin@robin.town>
2022-09-23 18:33:31 +02:00
Šimon Brandner 4625ed73cf Merge pull request #2695 from matrix-org/SimonBrandner/task/gc-merge 2022-09-23 17:43:23 +02:00
Šimon Brandner 6f7a72d69e Merge remote-tracking branch 'upstream/develop' into SimonBrandner/task/gc-merge
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-23 15:36:44 +02:00
Hugh Nimmo-Smith caadc6f95b Implementation of MSC3882 login token request (#2687) 2022-09-22 16:36:37 +01:00
Hugh Nimmo-Smith 39bc7e2bb3 Refer to explicit Matrix spec versions instead of Latest (#2668) 2022-09-22 15:28:11 +00:00
Hugh Nimmo-Smith 040f012350 Typings for MSC2965 OIDC provider discovery (#2424) 2022-09-22 16:27:13 +01:00
Hugh Nimmo-Smith 8b2b677f92 Add return type for loginFlows() (#2669)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-09-22 16:25:01 +01:00
Šimon Brandner 2a0ffe1223 Fix group call tests getting stuck (#2689) 2022-09-22 17:06:01 +02:00
Šimon Brandner 72a6ec0dd3 Add a few group call event handler tests (#2679) 2022-09-22 17:05:51 +02:00
Germain 2b1fab928b Add unstable device_id field for MSC3881 (#2688) 2022-09-22 15:44:54 +02:00
Germain 516f52c5a4 Support to remotely toggle push notifications (#2686) 2022-09-22 08:37:54 +00:00
Michael Telatynski 8599a98b47 Fix backpagination at end logic being spec non-conforming (#2680) 2022-09-21 16:35:07 +01:00
renovate[bot] 7e24cb6cae Update jest monorepo to v29.0.3 (#2682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 09:25:43 +01:00
renovate[bot] 38aa8d18c0 Update all (#2684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 09:13:36 +01:00
Germain 2967ee6309 Read receipts for threads (#2635) 2022-09-21 07:50:44 +00:00
renovate[bot] 2e10b6065c Update typescript-eslint monorepo to v5.38.0 (#2685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 08:42:30 +02:00
renovate[bot] 69057ee035 Update babel monorepo to v7.19.1 (#2681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 08:25:16 +02:00
Šimon Brandner 72b89fde6e Add test for call transfers (#2677) 2022-09-20 19:41:03 +02:00
Šimon Brandner c400dd4ff8 Add a few new GroupCall tests (#2678)
Co-authored-by: Robin <robin@robin.town>
2022-09-20 19:40:47 +02:00
RiotRobot f34e568a98 v19.6.0-rc.1 2022-09-20 13:55:51 +01:00
RiotRobot 6fd80ed3ed Prepare changelog for v19.6.0-rc.1 2022-09-20 13:55:50 +01:00
Kerry 9ff11d1f32 test typescriptification - last few unit test files (#2675)
* renamed:    spec/unit/crypto/verification/sas.spec.js -> spec/unit/crypto/verification/sas.spec.ts

* ts issues in sas.spec

* renamed:    spec/unit/crypto/verification/secret_request.spec.js -> spec/unit/crypto/verification/secret_request.spec.ts

* ts issues in secret_request.spec

* renamed:    spec/unit/crypto/verification/verification_request.spec.js -> spec/unit/crypto/verification/verification_request.spec.ts

* ts fix verification_req.spec

* renamed:    spec/browserify/sync-browserify.spec.js -> spec/browserify/sync-browserify.spec.ts

* fix strict

* formatting
2022-09-16 16:00:40 +00:00
Robin f41b7706e4 Upgrade matrix-widget-api (and fix the lockfile) (#2676) 2022-09-16 11:08:13 -04:00
Robin de694459be Target widget actions at a specific room (#2670)
Otherwise, the RoomWidgetClient class can end up accidentally sending and receiving events from rooms it didn't intend to, if it's an always-on-screen widget.
2022-09-16 10:26:03 -04:00
Kerry 41ab3337b5 test typescriptification - spec/unit/crypto/verification (#2673)
* renamed:    spec/unit/crypto/verification/request.spec.js -> spec/unit/crypto/verification/request.spec.ts

* renamed:    spec/unit/crypto/verification/qr_code.spec.js -> spec/unit/crypto/verification/qr_code.spec.ts

* renamed:    spec/unit/crypto/verification/InRoomChannel.spec.js -> spec/unit/crypto/verification/InRoomChannel.spec.ts

* fix ts issues in InRoomChannel.spec

* renamed:    spec/unit/crypto/verification/util.js -> spec/unit/crypto/verification/util.ts

* fix ts issues in util.t

* fix strict errors in util.ts

* js lint
2022-09-16 10:46:56 +02:00
David Baker 6fc9827b10 Add tests for ice candidate sending (#2674) 2022-09-16 09:26:37 +01:00
Michael Telatynski 1432e094c2 Update sonarcloud.yml 2022-09-15 14:26:22 +01:00
Michael Telatynski e09936cc9b Update sonarcloud.yml (#2671) 2022-09-15 14:16:15 +01:00
David Baker f52c5eb667 Unused imports from merge 2022-09-14 09:53:07 +01:00
David Baker c05cb3ad2b Merge branch 'develop' into robertlong/group-call 2022-09-14 09:51:43 +01:00
David Baker 586a313c8d Add tests for call answering / candidate sending (#2666)
* Add tests for call answering / candidate sending

* Remopve unused stuff

* Capitalise

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Capitalisation

* Capitalise

* Fix typescript strict error

* Actually fix TS strict error(?)

* TS strict mode try 3

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-14 09:42:57 +01:00
Germain d32d190a8a Change return type to avoid null strict error (#2630) 2022-09-14 09:02:16 +01:00
Michael Telatynski 53de8d5690 Fix reset_dependency for if the dep is indirect (#2664)
Like for in element-desktop
2022-09-14 08:57:52 +01:00
Michael Telatynski 829110b580 Update release-npm.yml (#2665) 2022-09-14 08:57:36 +01:00
David Baker c605310b87 Prevent exception when muting (#2667)
Fixes https://github.com/vector-im/element-call/issues/578
2022-09-13 20:02:14 +01:00
David Baker 41cee6f1cc Fix race in creating calls (#2662)
* Fix race in creating calls

We ran an async function between checking for an existing call and
adding the new one to the map, so it would have been possible to
start creating another call while we were placing the first call.
This changes the code to add the call to the map as soon as we've
created it.

Also adds more logging.

* Switch to logger.debug

* Fix unit tests
2022-09-13 16:30:34 +01:00
RiotRobot 59c82cb679 Resetting package fields for development 2022-09-13 12:32:41 +01:00
RiotRobot 6d3741d55c Merge branch 'master' into develop 2022-09-13 12:32:37 +01:00
RiotRobot 43213fec78 v19.5.0 2022-09-13 12:31:14 +01:00
RiotRobot c8438ff6da Prepare changelog for v19.5.0 2022-09-13 12:31:13 +01:00
RiotRobot a1d0f037e2 Fix release.sh check_dependency 2022-09-13 12:26:03 +01:00
Šimon Brandner fb565f301b Remove support for unstable private read receipts (#2624) 2022-09-12 18:04:14 +02:00
Michael Telatynski afa3b37ad5 Revert "Add login flow types from matrix-react-sdk (#2633)" (#2661)
* Revert "Add login flow types from matrix-react-sdk (#2633)"

This reverts commit 583e4808

* Leave login flow types, only revert method return type

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2022-09-12 11:25:39 +01:00
David Baker 3e1e99f8e5 Fix import in failed merge 2022-09-12 10:34:35 +01:00
David Baker 276849f068 Merge branch 'develop' into robertlong/group-call 2022-09-12 10:03:48 +01:00
David Baker 37118991f5 Add test for removing RTX codec (#2660)
* Add test for removing RTX codec

* Use mocked to cast
2022-09-12 09:40:28 +01:00
Hugh Nimmo-Smith a57c430b09 Implementation of MSC3824 to add action= param on SSO login (#2398)
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-11 21:18:33 +00:00
Hugh Nimmo-Smith 583e48086b Add login flow types from matrix-react-sdk (#2633)
Co-authored-by: Faye Duxovni <fayed@element.io>
2022-09-11 21:50:10 +01:00
David Baker 00629e6dc9 Test fallback screensharing (#2659)
* Test fallback screensharing

* Test replacetrack is called

* Unused import

* Return type

* Fix other test after new track IDs
2022-09-09 21:15:34 +01:00
David Baker 02f6a09bcf Test active speaker events (#2658)
Fixes https://github.com/vector-im/element-call/issues/527
2022-09-09 18:57:25 +01:00
Robin b22c671fee Add a property aggregating all names of a NamespacedValue (#2656)
For convenience
2022-09-09 09:55:17 -04:00
Robin 36a6117ee2 Misc fixes for group call widgets (#2657)
* Fix GroupCallEventHandler in matryoshka mode

GroupCallEventHandler needs to see a 'Syncing' event before it starts handling any events, so emit one immediately in matryoshka mode.

* Implement joinRoom on RoomWidgetClient

Element Call has undergone some changes to how it loads rooms, meaning that this method must be implemented for the app to work in matryoshka mode.

* Allow audio and video to be muted before local call feed exists

This is desirable for the Element Web integration of Element Call, because we need to be able to mute our devices before ever joining the call or creating a call feed, if the users requests it.

* Fix a strict mode error
2022-09-09 09:53:27 -04:00
David Baker aebe26db96 GroupCallEventhandler Tests (#2654)
* GroupCallEventhandler Tests

Fixes https://github.com/vector-im/element-call/issues/545

* Fix long line

* Fix strict mode error

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

* Fix typo

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

Co-authored-by: Robin <robin@robin.town>
2022-09-08 21:46:28 +01:00
David Baker 60e175a0e0 Merge branch 'develop' into robertlong/group-call 2022-09-08 20:52:08 +01:00
David Baker 1b86acb2fb Fix import locations (#2655)
These were causing circular references in the group call branch
2022-09-08 20:51:42 +01:00
David Baker d950cda05c Merge branch 'develop' into robertlong/group-call 2022-09-08 15:03:55 +01:00
renovate[bot] d2f7a2575e Update all (major) (#2651)
* Update all

* Pin p-retry once more

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-09-08 09:52:50 +00:00
David Baker 83c848093f MediaHandler Tests (#2646)
* MediaHandler Tests, part 1

Haven't got through all the methods yet

For https://github.com/vector-im/element-call/issues/544

* Didn't need these in the end

* Rest of the media handler tests

* getUserMediaStream takes args

* use mockResolvedValue

* Add .off & reuse the mock we already made

* Re-use mock handler again

* Move updateLocalUsermediaStream to beforeEach

* add .off

* Add types

* Add more .offs
2022-09-07 15:56:38 +01:00
Michael Telatynski 8490f72488 Tweak backport labels (#2652) 2022-09-07 11:51:11 +00:00
kegsay d87e53858b Merge pull request #2628 from matrix-org/kegan/ss-member-counts
sliding sync: add invited|joined_count
2022-09-07 11:22:47 +02:00
David Teller 917e8c01d8 Base support for MSC3847: Ignore invites with policy rooms (#2626)
* Base support for MSC3847: Ignore invites with policy rooms

Type: enhancement

* Base support for MSC3847: Ignore invites with policy rooms

Type: enhancement

* WIP: Applying feedback

* WIP: Applying feedback

* WIP: CI linter gives me different errors, weird

* WIP: A little more linting
2022-09-06 22:17:42 -06:00
renovate[bot] eb3309db43 Update jest monorepo to v29 (#2649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 22:02:40 -06:00
renovate[bot] 65741d7860 Update all (#2647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 22:05:37 +01:00
David Baker fa6f70f708 Log ID instead of object (#2643)
as otherwise it recurses and logs the entire client + store
2022-09-06 18:09:34 +01:00
renovate[bot] a7a264f4e7 Update typescript-eslint monorepo to v5.36.2 (#2645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 17:53:05 +01:00
renovate[bot] 0fa125b60a Update babel monorepo to v7.19.0 (#2644)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-06 17:52:57 +01:00
David Baker 98d119d6e1 Add client.waitUntilRoomReadyForGroupCalls() (#2641)
See comment, although this still feels like a poor solution to the
problem. Might be better if the js-sdk processed everything internally
before emitting the 'Room' event (or indeed before joinRoom resolved)
so the app knows everything is ready when it gets that event.
2022-09-06 13:54:48 +01:00
Michael Telatynski 8aee884d03 Fix handling of remote echoes doubling up (#2639)
* Fix handling of remote echoes doubling up

* Simplify code

* Make TSC strict happier

* Update `timelineWasEmpty` to match type

* Add tests

* Add tests

* Add lowly `!`
2022-09-06 13:27:24 +01:00
RiotRobot a8fd0f3d13 Fix release.sh check_dependency order 2022-09-06 12:48:52 +01:00
Michael Telatynski 876491e38d Remove redundant concurrency 2022-09-06 12:36:54 +01:00
RiotRobot 71fcd9f35b v19.5.0-rc.4 2022-09-06 12:35:27 +01:00
RiotRobot 193ff0b4d1 Prepare changelog for v19.5.0-rc.4 2022-09-06 12:35:27 +01:00
RiotRobot f616022d07 Fix self-referencing gha concurrency leading to blocked jobs 2022-09-06 12:34:32 +01:00
RiotRobot 3c206c0b85 v19.5.0-rc.3 2022-09-06 12:25:58 +01:00
RiotRobot a8c4ff473a Prepare changelog for v19.5.0-rc.3 2022-09-06 12:25:57 +01:00
RiotRobot 289a930cda Fix release.sh 2022-09-06 12:25:07 +01:00
RiotRobot 8ba30bc4ef v19.5.0-rc.2 2022-09-06 12:21:02 +01:00
RiotRobot 6e28634819 Prepare changelog for v19.5.0-rc.2 2022-09-06 12:21:01 +01:00
RiotRobot b1e70c5404 Fix release.sh 2022-09-06 12:20:41 +01:00
RiotRobot b11c502a40 Merge branch 'develop' into staging 2022-09-06 12:18:10 +01:00
Michael Telatynski 167f51c8cd Fix release script for layers without a release_config.yaml file (#2642) 2022-09-06 12:17:40 +01:00
RiotRobot 5d2753241e Merge branch 'develop' into staging 2022-09-06 12:11:27 +01:00
Michael Telatynski 274fe447fd Simplify releases: move npm publishing to gha, consolidate scripts (#2616)
* Remove stale comment re dependency

* Move npm publishing from release.sh to GHA

* Extract js-sdk & react-sdk post release steps

* Consolidate release subproject upgrade management
2022-09-06 12:10:26 +01:00
RiotRobot c0f1849a83 v19.5.0-rc.1 2022-09-06 11:28:19 +01:00
RiotRobot 7b2b618d26 Prepare changelog for v19.5.0-rc.1 2022-09-06 11:28:19 +01:00
David Baker aca51fd8a3 Test call mute status set on call state chnage (#2638) 2022-09-05 17:06:49 +01:00
David Baker c78631bdee Test that calls in a group call are retried (#2637)
* Test that calls in a group call are retried

* Add new flushpromises file
2022-09-05 09:45:32 +01:00
Kerry 37187ef347 Test typescriptification - room-member and room-state (#2601)
* renamed:    spec/MockStorageApi.js -> spec/MockStorageApi.ts

* renamed:    spec/olm-loader.js -> spec/olm-loader.t

* renamed:    spec/unit/room-state.spec.js -> spec/unit/room-state.spec.ts

* ts fixes in room-state.spec

* renamed:    spec/unit/room-member.spec.js -> spec/unit/room-member.spec.ts

* ts fixes in room-member.spec

* strict mode fixes for MockStorageApi

* strict ts fixes in room-state

* strict errors
2022-09-05 10:38:05 +02:00
David Baker 0d6a93b5f6 Refactor the group call placing calls test (#2636)
Add some types & use mock-typed versions directly - it's clearer which
client we're making assertions about.
2022-09-02 15:33:22 +01:00
David Baker 40ecfa7932 Test disabling screenshare in group calls (#2634)
Also add a few more types
2022-09-02 12:57:29 +01:00
3nprob e87ce873b0 utils: Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal (#2586)
* utils: Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal

* Fix bugs in sync test

This test wrongly asserted that `initialSyncLimit` would be used to make a filter
It is used only for the initial sync inline filter, and not in POST /filter

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-09-01 21:36:24 +00:00
David Baker d656b848f8 Wait for client to start syncing before making group calls (#2632)
As hopefully explained in comment

Fixes https://github.com/matrix-org/matrix-js-sdk/issues/2589
2022-09-01 17:57:38 +01:00
David Baker 0981652de4 Add GroupCallEventHandlerEvent.Room (#2631)
* Add GroupCalEventHandlerEvent.Room

Emit an event when the group call event handler has processed all
pending group calls.

* Remove unused return value

* Add void return type

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-01 16:18:37 +01:00
Michael Weimann 207171efd6 Link contributing to Element Web (#2621) 2022-09-01 13:49:47 +02:00
Travis Ralston 8cc5efdf46 Update CHANGELOG.md 2022-08-31 11:28:56 -06:00
RiotRobot cbcf47d5c0 Resetting package fields for development 2022-08-31 16:26:39 +01:00
RiotRobot bbaa0e6536 Merge branch 'master' into develop 2022-08-31 16:26:39 +01:00
RiotRobot 1efeb1ec0e v19.4.0 2022-08-31 16:24:27 +01:00
RiotRobot 06e8d98911 Prepare changelog for v19.4.0 2022-08-31 16:24:26 +01:00
Travis Ralston 8716c1ab9b Convert several internal maps to real maps 2022-08-31 09:21:46 -06:00
David Baker db32420d16 Add logging to diagnose connection issue (#2629)
For https://github.com/vector-im/element-call/issues/559
2022-08-31 13:40:55 +01:00
David Baker d5b82e343a Add types to the call unit test suites (#2627)
* Add types to the call unit test suites

Still involves quite a few casts to any unfortunately as it turns
out we access quite a few private methods on the Call class in these
tests.

* Remove commented line & use better expect syntax

* Replace more calls.length with toHaveBeenCalled

* Remove mistakenly added id field
2022-08-31 11:15:13 +01:00
Kegan Dougal ac7f505a2b Use the right nulls 2022-08-30 18:13:37 +01:00
Kegan Dougal b5576758e4 Even more strict TS type checking 2022-08-30 18:07:53 +01:00
Kegan Dougal c32a83fdac Linting 2022-08-30 18:02:56 +01:00
Kegan Dougal 2d9556c1bb More strict TS type checking 2022-08-30 18:00:03 +01:00
Kegan Dougal 818b70554a Strict TS checks 2022-08-30 17:50:48 +01:00
Kegan Dougal 725336ffc6 sliding sync: add invited|joined_count
This is critical for calculating client-side push rules correctly.
Without it, the push processor may think rooms have a different
number of members, resulting typically in annoying failure modes
where rooms constantly make noises because the code thinks they
are 1:1 rooms.
2022-08-30 17:41:54 +01:00
renovate[bot] 1fbd8983ed Lock file maintenance (#2625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-29 00:00:48 -04:00
Faye Duxovni 1c77816dbd Use deep equality comparisons when searching for outgoing key requests by target (#2623) 2022-08-27 03:13:08 +00:00
David Baker 965f4fb13b Fix ICE end-of-candidates messages (#2622)
* Fix ICE end-of-candidates messages

We were casting a POJO to an RTCIceCandidate for the dummy
end-of-candidates candidate, but https://github.com/matrix-org/matrix-js-sdk/pull/2473
started calling .toJSON() on these objects.

Store separately whether we've seen the end of candidates rather than
adding on a dummy candidate object.

A test for this will follow, but a) I want to get this fix out and
b) I'm currently rewriting the call test file to add typing.

Fixes https://github.com/vector-im/element-call/issues/553

* Remove hacks for testing

* Switch if branches
2022-08-26 10:04:07 +01:00
Michael Telatynski c1160f40c2 Tweak tsc-strict config (#2620) 2022-08-25 09:49:58 +01:00
David Baker 9e1b126854 1:1 screenshare tests (#2617)
* 1:1 screenshare tests

Fixes https://github.com/vector-im/element-call/issues/548

* Always hang up calls after tests

to prevent hanging tests

Also fix a null dereference as we may not have an invitee or opponent
member when sending voip events if not using to-device messages.

* use mockImplementationOnce

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

* use mockImplementationOnce

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

* Add type on mock

* Add corresponding call.off

* Merge enable & disable screenshare tests

Co-authored-by: Robin <robin@robin.town>
2022-08-24 15:45:53 +01:00
David Baker c527f85fb1 Revert 4a294c9dd3
Pushed to wrong branch
2022-08-23 22:03:25 +01:00
David Baker 4a294c9dd3 1:1 screenshare tests
Fixes https://github.com/vector-im/element-call/issues/548
2022-08-23 22:02:32 +01:00
Michael Telatynski b789cc5933 Refactor Sync and fix initialSyncLimit (#2587)
* Small tidy-up to sync.ts

* Convert doSync into a while loop

* Apply `initialSyncLimit` only to initial syncs

* Convert matrix-client-syncing spec to TS

* Add tests around initial sync filtering

* Switch confusing filterId field for `filter`

* Tweak doSync error control flow

* Fix error control flow intricacies

* use includes

* Add tests

* Fix some strict mode errors

* Fix more strict mode errors

* Fix some strict mode errors
2022-08-23 16:25:54 +01:00
Jonathan Otto 5e4474b959 Fix room membership race with PREPARED event (#2613)
* Fix room membership race with PREPARED event

See the call site of the original triggering event of this function: https://github.com/matrix-org/matrix-js-sdk/blob/b265d795a427c6d30ccdf279a09f7836509df863/src/sliding-sync.ts#L789-L806

I think the bug is current code assumes downstream event listeners of `SlidingSyncEvent.RoomData` have synchronous execution so that by the time it emits `SlidingSyncState.Complete`, and eventually `SyncState.Prepared` the room state is correct. But since SlidingSyncSdk's `processRoomData` is async, and the membership field was being set after the async, it looks like `SlidingSyncState.Complete` was being fired before the membership field was set.

* Rm whitespace
2022-08-23 15:01:33 +00:00
Kegan Dougal 4059b5bfba Merge branch 'develop' into kegan/sync-v3 2022-08-23 15:57:08 +01:00
Michael Telatynski 8e646ea584 Add static analysis for tsc --strict (#2615)
* Initial attempt at CI to annotate new TSC errors

* Make tsconfig file valid

* enable debug

* Specify commit

* Fix commit specification

* Switch back to main

* Tweak permissions

* Add strict mode failure

* Attempt number two

* Fix ts-extra-args

* Add static analysis for tsc --strict
2022-08-23 14:02:50 +01:00
RiotRobot 528e9343ae v19.4.0-rc.1 2022-08-23 10:53:02 +01:00
RiotRobot 6571b6a1ab Prepare changelog for v19.4.0-rc.1 2022-08-23 10:53:01 +01:00
kegsay 1df329df7c Merge pull request #2610 from matrix-org/kegan/ss-bugfix
sliding sync: handle lone DELETE and INSERT operations
2022-08-23 08:42:34 +01:00
kegsay 760eeaeed7 Merge pull request #2612 from matrix-org/kegan/ss-tags
Add tags and not_tags to the list of valid sliding sync filters
2022-08-23 08:42:14 +01:00
renovate[bot] 438fc70615 Update all (#2614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-23 08:42:56 +02:00
David Baker be94f5ea93 Fix imports 2022-08-22 20:30:27 +01:00
David Baker 5f9369abee Merge branch 'develop' into robertlong/group-call 2022-08-22 20:19:53 +01:00
David Baker e7a7ec0673 Test call timeouts (#2611)
Fixes https://github.com/vector-im/element-call/issues/547
2022-08-22 20:17:19 +01:00
Kegan Dougal de3b3960d2 Add tags and not_tags to the list of valid sliding sync filters 2022-08-22 18:20:51 +01:00
Robin b265d795a4 Re-emit room state events on rooms (#2607)
* Re-emit room state events on rooms

This also fixes some potential memory leaks and abuse of
removeAllListeners in sync.ts.

* Remove some stray whitespace

* Deduplicate some code to appease SonarCloud

* Name helper function more explicitly
2022-08-22 17:04:32 +02:00
Michael Telatynski eb79f6246d Add ability to override built in room name generator for an i18n'able one (#2609)
* Add ability to override built in room name generator for an i18n'able one

* Add tests
2022-08-22 14:39:04 +01:00
Kegan Dougal 37f8f736e0 sliding sync: handle lone DELETE and INSERT operations
If you leave a room you can get a lone DELETE op.
If you join a room you can get a lone INSERT op.

Up until now, we've assumed these operations happen at the ends
of the list (e.g [0] or [length-1]) which is not guaranteed as it
depends on the sort order (e.g sort alphabetically and join a room
called 'D'). In this scenario, the indexes would not be tracked
correctly. Fixed with integration tests.
2022-08-22 14:37:10 +01:00
David Baker 92cd84fc0c Add unit tests for hangup / reject (#2606)
* Add unit tests for hangup / reject

Fixes https://github.com/vector-im/element-call/issues/537

* Fix some bugs where we carried on with the call after it had been ended
2022-08-22 13:29:54 +01:00
renovate[bot] 4b1a443f90 Lock file maintenance (#2608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-22 09:30:03 +01:00
Kegan Dougal 3a120f8fb8 Assert pos values as well 2022-08-19 18:19:24 +01:00
Kegan Dougal d2535b8516 Linting 2022-08-19 18:15:33 +01:00
Kegan Dougal 2cda229bc4 Automatically reconnect sessions when sliding sync expires them
This can happen when you close your laptop overnight,
as the server will not hold onto in-memory resources
for your connection indefinitely. When this happen,
the server will HTTP 400 you with "session expired".

At this point, it is no longer safe to remember anything
and you must forget everything and resend any sticky
parameters. This commit does the sticky parameters and
re-establishes the connection, but it may need additional
work to make the JS SDK forget now invalid data.
2022-08-19 17:33:07 +01:00
Šimon Brandner 45e56f8cc3 Add disposed to CallFeed (#2604) 2022-08-19 17:33:55 +02:00
Robin e95947dc73 Update lockfile (#2603) 2022-08-19 08:22:37 -04:00
Šimon Brandner 448a5c9a77 Add screensharing tests (#2598) 2022-08-18 10:42:03 +02:00
David Baker 9589a97952 Test muting in PTT mode (#2599)
Fixes https://github.com/vector-im/element-call/issues/523
2022-08-17 18:09:46 +01:00
David Baker 2566c40e96 Add tests for incoming calls in group calls (#2597)
* Add tests for incoming calls in group calls

Inspiration wwlecome for the renamed describe group which we're
really abusing for a bunch of things that happen to have the same
dependencies.

Fixes https://github.com/vector-im/element-call/issues/532

* Extract incoming call tests out into their own describe

and get the lexicographical ordering to match who should be calling who

* Trailing space
2022-08-17 15:10:03 +01:00
David Baker 099cac0162 Merge branch 'develop' into robertlong/group-call 2022-08-17 12:34:25 +01:00
David Baker e4cf5b26ee Add test for updateLocalUsermediaStream (#2596) 2022-08-17 12:33:11 +01:00
Šimon Brandner 3ae974e23e Remove duplicate log of answering a call (#2595) 2022-08-17 10:23:06 +00:00
Šimon Brandner c698317f3f Add group call tests for muting (#2590) 2022-08-17 10:59:54 +02:00
David Baker e8f682f452 Test placing a call in a group call (#2593)
* Test placing a call in a group call

Refactors a bit of the call testing stuff

Fixes https://github.com/vector-im/element-call/issues/521

* Unused imports

* Use expect.toHaveBeenCalledWith()

* Types

* More types

* Add comment on mock typing

* Use toHaveBeenCalledWith()

* Initialise groupcall & room in beforeEach

* Initialise mockMediahandler sensibly

* Add type params to mock

* Rename mute tests

* Move comment

* Join / leave in parallel

* Remove leftover expect
2022-08-16 18:22:36 +01:00
RiotRobot 566b4ba56c Resetting package fields for development 2022-08-16 15:25:58 +01:00
RiotRobot 13291f33d2 Merge branch 'master' into develop 2022-08-16 15:25:57 +01:00
RiotRobot 8502759e3e v19.3.0 2022-08-16 15:23:33 +01:00
RiotRobot 24f9075a84 Prepare changelog for v19.3.0 2022-08-16 15:23:33 +01:00
ElementRobot d18aae09c8 Fix: Handle parsing of a beacon info event without asset (#2591) (#2592)
* test case

* handle missing beacon info asset

* default beacon info asset type to self

* make BeaconLocationState.assetType optional

(cherry picked from commit be3e731499)

Co-authored-by: Kerry <kerrya@element.io>
2022-08-16 14:50:49 +01:00
Kerry be3e731499 Fix: Handle parsing of a beacon info event without asset (#2591)
* test case

* handle missing beacon info asset

* default beacon info asset type to self

* make BeaconLocationState.assetType optional
2022-08-16 15:33:19 +02:00
RiotRobot a9f2ae6b55 v19.3.0-rc.2 2022-08-12 13:24:20 +01:00
RiotRobot b254ca7fc8 Prepare changelog for v19.3.0-rc.2 2022-08-12 13:24:19 +01:00
3nprob 3f6f5b69c7 Improve test coverage and modernize style for interactive-auth (#2574)
* style: address no-mixed-operators errors,minor style improvements

* test: Fix async interactive-auth tests, add test case

* tests: Fix incorrectly stringified mock response

* pushprocessor: style update

* use async primitives in interactive-auth-spec

* lint

* fixup: remove duplicate test

* add test case for no-flow-with-session for interactive-auth

* interactive-auth: handle non-existing error.data

* async test fix

* test: add dummyauth test

* add testing for errcode

* Revert "pushprocessor: style update"

This reverts commit 3ed0fdfb73ae55b725aa7c74d9cab35fb96c9178.

* add testcase for missing error data

* test: move sessionId assignment

* Add tests to improve coverage for interactive-auth

* pushprocessor: style update
2022-08-11 15:29:53 +01:00
ElementRobot 0e8bd3f02d Fix finding event read up to if stable private read receipts is missing (#2585) (#2588)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
(cherry picked from commit 478270b225)

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-08-11 12:36:54 +00:00
Šimon Brandner 478270b225 Fix finding event read up to if stable private read receipts is missing (#2585)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-08-11 13:30:51 +01:00
kegsay 9eb72908a7 Merge pull request #2583 from matrix-org/kegan/sync-v3
sliding sync bugfix: ensure history is treated as history and not live events
2022-08-10 20:00:53 +01:00
Kegan Dougal 2728d74771 Review comments 2022-08-10 19:53:36 +01:00
David Baker 020743141b Tidy up imports (#2584)
Duplicate 'call' imports
2022-08-10 18:07:49 +01:00
David Baker 5f5a9b1a43 Merge branch 'develop' into robertlong/group-call 2022-08-10 14:31:39 +01:00
Kegan Dougal edcef9364c Only add events if there are some; set the pagination token for faster scrollback 2022-08-10 12:43:47 +01:00
Kegan Dougal 1635ac9971 sliding sync bugfix: ensure history is treated as history and not live events
with tests
2022-08-10 12:32:28 +01:00
kegsay 8f13df2dd9 Merge pull request #2567 from matrix-org/kegan/sync-v3
Add txn_id support to sliding sync
2022-08-10 12:16:47 +01:00
Kegan Dougal fa9f078a75 Review comments 2022-08-10 11:53:13 +01:00
Michael Telatynski 055af933cc Update backport.yml 2022-08-10 11:41:44 +01:00
Michael Telatynski 1ba2730e25 Update backport.yml (#2582) 2022-08-10 11:33:51 +01:00
github-actions[bot] 1c9d644a23 Update jsdoc.yml (#2577) (#2581)
(cherry picked from commit 9ee94c9902)

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-08-10 09:31:02 +01:00
Michael Telatynski e29e0d15a5 Set up basic Backporting action (#2580)
* Create backport.yml

* Update backport.yml
2022-08-10 09:21:39 +01:00
Michael Telatynski 9ee94c9902 Update jsdoc.yml (#2577) 2022-08-10 09:04:38 +01:00
renovate[bot] 1645867ea6 Update babel monorepo to v7.18.10 (#2578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-10 07:27:31 +01:00
renovate[bot] 24d4181a08 Update typescript-eslint monorepo to v5.33.0 (#2579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-09 15:42:40 -04:00
RiotRobot f576a9f2e9 v19.3.0-rc.1 2022-08-09 17:03:34 +01:00
RiotRobot fed121b0aa Prepare changelog for v19.3.0-rc.1 2022-08-09 17:03:34 +01:00
Robin 3334c01191 Support nested Matrix clients via the widget API (#2473)
* WIP RoomWidgetClient

* Wait for the widget API to become ready before backfilling

* Add support for sending user-defined encrypted to-device messages

This is a port of the same change from the robertlong/group-call branch.

* Fix tests

* Emit an event when the client receives TURN servers

* Expose the method in MatrixClient

* Override the encryptAndSendToDevices method

* Add support for TURN servers in embedded mode and make calls mostly work

* Don't put unclonable objects into VoIP events

RoomWidget clients were unable to send m.call.candidate events, because
the candidate objects were not clonable for use with postMessage.
Converting such objects to their canonical JSON form before attempting
to send them over the wire solves this.

* Fix types

* Fix more types

* Fix lint

* Upgrade matrix-widget-api

* Save lockfile

* Untangle dependencies to fix tests

* Add some preliminary tests

* Fix tests

* Fix indirect export

* Add more tests

* Resolve TODOs

* Add queueToDevice to RoomWidgetClient
2022-08-09 09:45:58 -04:00
David Baker 0b8de251bf Add basic creation / entering tests for group calls (#2575)
* Add basic creation / entering tests for group calls

* Missing space

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

* Assert more of the group call member event

and also move call leaving to a finally so it doesn't leaving a call
hagning if it fails.

Co-authored-by: Robin <robin@robin.town>
2022-08-09 13:43:02 +01:00
David Baker 88ce017333 Fix return types of event sending functions (#2576)
These had somehow got mixed up so the type check was failing.
Nothing uses the response return type, so just return void.
2022-08-09 13:11:59 +01:00
3nprob 3e37c74264 Document where new linting rules go (#2573) 2022-08-09 09:59:17 +02:00
Faye Duxovni 3762c20aad Revert "Always block sending keys to unverified devices of verified users (#2562)" (#2571)
This will be rolled out again later with more accompanying UI adjustments, including clearer error messages and possibly the option to disable it per-room.
2022-08-08 12:27:41 -04:00
Kegan Dougal 2596999cb8 Review comments 2022-08-08 14:26:24 +01:00
Kegan Dougal a3248c0aa1 Merge branch 'develop' into kegan/sync-v3 2022-08-08 12:51:03 +01:00
Matthew Hodgson c96f1ba22b Merge pull request #2569 from matrix-org/matthew/avoid-sync-overlaps
Don't load the sync accumulator if there's already a sync persist in flight
2022-08-08 10:18:39 +01:00
Matthew Hodgson 43c81358b2 don't load the sync accumulator if there's already a sync persist in flight
this should hopefully reduce chances of
https://github.com/vector-im/element-web/issues/21541 a bit more
as we were incorrectly loading the sync accumulator even
if a sync persist was already in flight, thus wasting RAM
and increasing the chance of the renderer process OOMing
2022-08-07 01:13:06 +01:00
Kegan Dougal e05f9b5815 Add txn_id support to sliding sync
This allows clients to know when a request has been applied
on the server. This allows us to change `resend(): void` to
`resend(): Promise<string>` which resolves/rejects with the
transaction ID when it has been applied/not.
2022-08-05 17:28:02 +01:00
Šimon Brandner 6316a6ae44 Add support for stable prefixes for MSC2285 (#2524)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-08-05 17:33:49 +02:00
David Baker 471f174889 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-08-05 15:59:01 +01:00
David Baker 575b416856 Simplify encryptAndSendToDevices (#2566)
It went to quite a lot of effort to gather a bunch of information to
return, but the only thing using it already had all that info anyway.
2022-08-05 15:58:33 +01:00
David Baker 7b7f8c1592 Increase timeout to try & avoid flakiness in queueToDevice test (#2565)
https://github.com/matrix-org/matrix-js-sdk/issues/2561
2022-08-05 15:35:29 +01:00
David Baker c0dacb5037 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-08-05 11:06:10 +01:00
David Baker 2cc51e0db7 Merge changes from develop (#2563)
* Prepare changelog for v19.2.0-rc.1

* v19.2.0-rc.1

* Sliding sync: add missing filters from latest MSC

* Gracefully handle missing room_ids

* Prepare changelog for v19.2.0

* v19.2.0

* Resetting package fields for development

* Use EventType enum values instead of hardcoded strings (#2557)

* Retry to-device messages (#2549)

* Retry to-device messages

This adds a queueToDevice API alongside sendToDevice which is a
much higher-level API that adds the messages to a queue, stored in
persistent storage, and retries them periodically. Also converts
sending of megolm keys to use the new API.

Other uses of sendToDevice are nopt converted in this PR, but could
be later.

Requires https://github.com/matrix-org/matrix-mock-request/pull/17

* Bump matrix-mock-request

* Add more waits to make indexeddb tests pass

* Switch some test expectations to queueToDevice

* Stop straight away if the client has been stopped

Hopefully will fix tests being flakey and logging after tests have
finished.

* Add return types & fix constant usage

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Stop the client in all test cases

Co-authored-by: Germain <germains@element.io>

* Add support for sending user-defined encrypted to-device messages (#2528)

* Add support for sending user-defined encrypted to-device messages

This is a port of the same change from the robertlong/group-call branch.

* Fix tests

* Expose the method in MatrixClient

* Fix a code smell

* Fix types

* Test the MatrixClient method

* Fix some types in Crypto test suite

* Test the Crypto method

* Fix tests

* Upgrade matrix-mock-request

* Move useRealTimers to afterEach

* Remove stream-replacement (#2551)

* Reintroduce setNewStream method, fix test, update yarn.lock

Co-authored-by: RiotRobot <releases@riot.im>
Co-authored-by: Kegan Dougal <kegan@matrix.org>
Co-authored-by: Germain <germains@element.io>
Co-authored-by: Robin <robin@robin.town>
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-08-04 17:28:54 +01:00
David Baker 3907d1c28f Fix output after test finished (#2564)
* Fix output after test finished

As per comment

* Add more flushes
2022-08-04 17:28:42 +01:00
Robin c629d2f60e Emit an event when the client receives TURN servers (#2529)
* Emit an event when the client receives TURN servers

* Add tests

* Fix lints
2022-08-04 11:44:10 -04:00
Faye Duxovni 43b453804b Always block sending keys to unverified devices of verified users (#2562) 2022-08-04 11:11:12 -04:00
Šimon Brandner d867affc40 Remove stream-replacement (#2551) 2022-08-03 21:45:37 +02:00
Robin c36bfc821c Add support for sending user-defined encrypted to-device messages (#2528)
* Add support for sending user-defined encrypted to-device messages

This is a port of the same change from the robertlong/group-call branch.

* Fix tests

* Expose the method in MatrixClient

* Fix a code smell

* Fix types

* Test the MatrixClient method

* Fix some types in Crypto test suite

* Test the Crypto method

* Fix tests

* Upgrade matrix-mock-request

* Move useRealTimers to afterEach
2022-08-03 16:16:48 +00:00
David Baker 7e784da00a Retry to-device messages (#2549)
* Retry to-device messages

This adds a queueToDevice API alongside sendToDevice which is a
much higher-level API that adds the messages to a queue, stored in
persistent storage, and retries them periodically. Also converts
sending of megolm keys to use the new API.

Other uses of sendToDevice are nopt converted in this PR, but could
be later.

Requires https://github.com/matrix-org/matrix-mock-request/pull/17

* Bump matrix-mock-request

* Add more waits to make indexeddb tests pass

* Switch some test expectations to queueToDevice

* Stop straight away if the client has been stopped

Hopefully will fix tests being flakey and logging after tests have
finished.

* Add return types & fix constant usage

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Stop the client in all test cases

Co-authored-by: Germain <germains@element.io>
2022-08-03 13:32:58 +01:00
Germain b79f469008 Use EventType enum values instead of hardcoded strings (#2557) 2022-08-03 08:54:11 +00:00
RiotRobot cf33569a21 Resetting package fields for development 2022-08-02 17:01:12 +01:00
RiotRobot fb0a0c66c8 Merge branch 'master' into develop 2022-08-02 17:01:11 +01:00
RiotRobot aac0023338 v19.2.0 2022-08-02 16:58:54 +01:00
RiotRobot e3873ddef5 Prepare changelog for v19.2.0 2022-08-02 16:58:53 +01:00
kegsay f0991348e2 Merge pull request #2555 from matrix-org/kegan/sync-v3
Sliding sync: add missing filters from latest MSC
2022-08-02 14:54:37 +01:00
Šimon Brandner 22c5999fed Delint group calls (#2554) 2022-08-01 18:45:14 +02:00
Kegan Dougal fa6708c27e Gracefully handle missing room_ids 2022-08-01 16:34:11 +01:00
Kegan Dougal 4427201326 Sliding sync: add missing filters from latest MSC 2022-08-01 16:30:33 +01:00
David Baker b711781f16 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-07-29 10:56:01 +01:00
Kerry 4a4241806e test typescriptification - autodiscovery / crypto specs (#2550)
* spec/unit/autodiscovery.spec.js -> spec/unit/autodiscovery.spec.ts

* fix ts in autodiscovery.spec

* renamed:    spec/unit/crypto.spec.js -> spec/unit/crypto.spec.ts

* fix ts in crypto.spec

* fix some strict errors
2022-07-29 09:11:01 +00:00
Šimon Brandner 8ba2d257ae Add support for audio sharing (#2530) 2022-07-28 18:12:11 +02:00
David Baker 3824f65d15 Prevent double mute status changed events (#2502) (#2522)
Audio & video mute status were set in separate calls but share a
mute status changed event, so you'd always get two mute status
changed events emitted. We could suppress events where the mute
status didn't change, but this would still get two events saying
the same thing when they both changed. Instead, merge setAudioMuted
& setVideoMuted into a single call that sets either or both.

Port of https://github.com/matrix-org/matrix-js-sdk/pull/2502 from
group call branch
2022-07-28 16:13:00 +01:00
David Baker 9e2e144530 Make SDP munging media type specific (#2526)
* Make SDP munging media type specific

We were trying to apply modifications to all media types which led
to confusing warning messages saying opus wasn't present (when it
was for the video stream). Make the modifications media-type specific
to avoid this.

* Make codec * mediatype into enums
2022-07-28 09:38:57 +01:00
Michael Telatynski 3c17e4a6d6 Use consolidated Renovate config (#2548)
* Delete renovate.json

* Create renovate.json
2022-07-28 08:13:02 +02:00
Kerry 75513d08de test typescriptification - misc (#2547)
* renamed:    spec/unit/login.spec.js -> spec/unit/login.spec.ts

* type test client

* renamed:    spec/unit/interactive-auth.spec.js -> spec/unit/interactive-auth.spec.ts

* fix ts issues in interactive-auth.spec

* renamed:    spec/unit/filter.spec.js -> spec/unit/filter.spec.ts

* fix ts in filter.spec

* renamed:    spec/unit/event.spec.js -> spec/unit/event.spec.ts

* ts in event.spec

* renamed:    spec/unit/pushprocessor.spec.js -> spec/unit/pushprocessor.spec.ts

* fix ts in pushprocessor.spec

* fix ts in realtime-callbacks.spec

* renamed:    spec/unit/content-repo.spec.js -> spec/unit/content-repo.spec.ts

* fix signature for getHttpUriForMxc

* pr fixes
2022-07-28 08:09:21 +02:00
Šimon Brandner 7cb3b40493 Use stable prefixes for MSC3827 (#2537) 2022-07-27 20:10:39 +02:00
Kerry ab89804c55 test typescriptification: unit/crypto/algorithm specs (#2538)
* typescriptify megolm.spec

* add copyright

* renamed:    spec/unit/crypto/algorithms/olm.spec.js -> spec/unit/crypto/algorithms/olm.spec.ts

* fix ts issues in olm.spec

* remove comment

* more types in megolm and olm specs
2022-07-27 17:43:17 +02:00
renovate[bot] ab6cf93c2b Lock file maintenance (#2546)
* Lock file maintenance

* Empty commit to retry CI

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Robin Townsend <robin@robin.town>
2022-07-27 15:17:00 +00:00
Kerry 4c80762e22 test typescriptification - timeline-window, scheduler, etc (#2539)
* spec/unit/user.spec.js -> spec/unit/user.spec.ts

* fix ts in user.spec

* renamed:    spec/unit/timeline-window.spec.js -> spec/unit/timeline-window.spec.ts

* overdo it fixing types in timeline-window.spec

* renamed spec/unit/sync-accumulator.spec.js spec/unit/sync-accumulator.spec.ts

* fix ts in sync-accumalator.spec

* spec/unit/scheduler.spec.js -> spec/unit/scheduler.spec.ts

* fix ts in scheduler.spec

* missed types in timeline-window spec
2022-07-27 15:10:20 +00:00
Michael Telatynski 1f7e80c68d Require confirmation when doing proper release when intending to make an RC (#2540) 2022-07-27 09:12:57 +01:00
renovate[bot] e91b879a69 Update typescript-eslint monorepo to v5.31.0 (#2544)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-26 17:45:10 +00:00
renovate[bot] 14885ba7a2 Update dependency @types/jest to v28.1.6 (#2543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-26 17:37:13 +00:00
renovate[bot] 0dda187d96 Update all (#2541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-26 17:30:10 +00:00
renovate[bot] 680d8cac4d Update babel monorepo to v7.18.9 (#2542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-26 18:16:53 +01:00
RiotRobot a7fd7fd539 v19.2.0-rc.1 2022-07-26 17:22:49 +01:00
RiotRobot 300d8b026a Prepare changelog for v19.2.0-rc.1 2022-07-26 17:22:48 +01:00
RiotRobot d5a15ac275 Resetting package fields for development 2022-07-26 16:12:26 +01:00
RiotRobot bbb5294b3b Merge branch 'master' into develop 2022-07-26 16:12:25 +01:00
RiotRobot 7731579796 v19.1.0 2022-07-26 16:08:28 +01:00
RiotRobot 55ab38a097 Prepare changelog for v19.1.0 2022-07-26 16:08:27 +01:00
Timo 38a6949e5d add missing events from reemitter to GroupCall (#2527) 2022-07-25 09:44:37 -07:00
Šimon Brandner e876482e62 Add local volume control (#2525) 2022-07-25 15:51:06 +02:00
Faye Duxovni 5367ee18fb Re-insert room IDs when decrypting bundled redaction events returned by /sync (#2531) 2022-07-21 10:55:20 +00:00
Faye Duxovni 45db39ec88 Rewrite megolm integration tests with async arrow functions (#2519) 2022-07-21 10:41:46 +00:00
renovate[bot] 32f55de383 Update dependency terser to v5.14.2 [SECURITY] (#2533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-21 08:57:07 +02:00
Hubert Chathi 7e8dfa56d0 Support fixed base64 in SAS verification (#2320) 2022-07-20 09:16:40 -04:00
Faye Duxovni 32bb4b1fc4 Typescriptify megolm integration tests (#2518) 2022-07-14 15:36:34 +00:00
renovate[bot] ae9bb6f27f Lock file maintenance (#2523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-13 15:58:11 +00:00
Travis Ralston 08ab51eeac Remove unstable support for m.room_key.withheld (#2512)
We no longer send or receive the unstable type.
2022-07-13 08:56:01 -06:00
renovate[bot] aa130c88da Update all (major) (#2517)
* Update all

* Pin p-retry due to ESM weirdness

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-07-13 08:16:28 -04:00
Michael Telatynski 9523978861 Update release.sh to support a staging branch (#2514) 2022-07-13 09:43:12 +01:00
Robin 5112340040 Correct the units in TURN servers expiry documentation (#2520)
As shown elsewhere in client.ts, turnServersExpiry really is in
milliseconds rather than seconds. It seems that other libraries like
matrix-react-sdk were already expecting it to be in milliseconds
anyways, so it's just the documentation that was wrong.
2022-07-12 18:48:44 +00:00
David Baker 544b1c6742 Merge develop into group call branch again (#2513)
* Send call version `1` as a string (#2471)

* test typescriptification - backup.spec (#2468)

* renamed:    spec/unit/crypto/crypto-utils.js -> spec/unit/crypto/crypto-utils.ts

* ts fixes in crypto-utils

* renamed:    spec/unit/crypto/backup.spec.js -> spec/unit/crypto/backup.spec.ts

* ts fixes in backup.spec

* remove fit

* remove debug

* Prepare changelog for v19.0.0-rc.1

* v19.0.0-rc.1

* Update jest monorepo (#2476)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Update all (#2475)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Update dependency @types/jest to v28 (#2478)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Fix call.collectCallStats() (#2480)

Regressed by https://github.com/matrix-org/matrix-js-sdk/pull/2352
(you can just use RTCStatsReport as an iterator directly (which
was was what that code was doing before) which uses entries(
which gives you key/value pairs, but using forEach gives you just
the value.

* Go back to forEach in collectcallstats (#2481)

Older typescript library doesn't know about .values() on the stats
object, so it was failing in react sdk which had an older typescript.
https://github.com/matrix-org/matrix-react-sdk/pull/8935 was an
attempt to upgrade it but did not seem to be helping on CI, despite
being fine locally.

* Update babel monorepo to v7.18.6 (#2477)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Expose KNOWN_SAFE_ROOM_VERSION (#2474)

* Fix return type on funcs in matrixClient to be optionally null (#2488)

* Update pull_request.yaml (#2490)

* Lock file maintenance (#2491)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Prepare changelog for v19.0.0

* v19.0.0

* Resetting package fields for development

* Improve VoIP integrations testing (#2495)

* Remove MSC3244 support (#2504)

* Actually store the identity server in the client when given as an option (#2503)

* Actually store the identity server in the client when given as an option

* Update requestRegisterEmailToken to a modern spec version too

* Properly re-insert room ID in bundled thread relation messages from sync (#2505)

Events returned by the `/sync` endpoint, including relations bundled with other events, may have their `room_id`s stripped out. This causes decryption errors if the IDs aren't repopulated.

Fixes vector-im/element-web#22094.

* Remove `setNow` from `realtime-callbacks.ts` (#2509)

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Remove dead code (#2510)

* Don't crash with undefined room in `processBeaconEvents()` (#2500)

* Add a basic PR checklist for all PRs (#2511)

It'll be mildly annoying for core developers who have to constantly remove or edit this, but it'll also serve as a good reminder to do these things.

Note that signoff is not required for core developers.

* Fix tests

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
Co-authored-by: Kerry <kerrya@element.io>
Co-authored-by: RiotRobot <releases@riot.im>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Weimann <michaelw@matrix.org>
Co-authored-by: texuf <texuf.eth@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Travis Ralston <travisr@matrix.org>
Co-authored-by: Faye Duxovni <fayed@element.io>
2022-07-12 19:27:41 +01:00
Faye Duxovni 6fb40d465e Typescriptify crypto integration tests (#2508) 2022-07-12 12:18:39 -04:00
kegsay 8d7eaa769a Add support for MSC3575: Sliding Sync (#2242)
* sliding sync: add client function and add stub sliding-sync.ts

Mostly c/p from sync.ts. Define interfaces for MSC3575 sliding
sync types. Complete WIP!

* Add core sliding sync classes

* Add integration tests for sliding sync api basics

* gut unused code; add more types

* Use SlidingSync in MatrixClient; stub functions for Sync

Enough to make ele-web actually load okay with 0 rooms.

* Start feeding through room data to the client

* Bugfixes so it sorta ish works

* Refactor the public API for sliding sync

Still needs some work but it's a start.

* Use EventEmitter for callbacks. Add ability to adjust lists and listen for list updates.

- Have atomic getList/setList operations on SlidingSync to update windows etc
- Add a list callback which is invoked with the list indicies and joined count.

* Add stub tests; add listenUntil to make tests easier to read

* No need to resend now

* Add more sliding sync tests; add new setListRanges function

* build tests upon one another to reduce boilerplate and c/p

* More thorough sliding sync tests

* Dependency inject SlidingSync in Client opts when calling startClient()

* Linting

* Fix crash when opts is undefined

* Fix up docs to make CI happy

* Remove all listeners when stop()d to allow for GC

* Add support for extensions

* Add ExtensionE2EE automatically if opts.crypto is present

* Add ExtensionToDevice automatically

* Bugfixes for to_device message processing

* default events to []

* bugfix: don't tightloop when the server is down

Caused by not detecting abort() correctly

* Return null for bad index positions

* Add getListData to get the initial calculated list response

* Add is_tombstoned

* More comments

* Add support for account data extension; rejig extension interface

* Handle invite_state

* Feed through prev_batch tokens

* Linting

* Fix tests

* Linting

* Iterate PR

* Iterate tests and remove unused code

* Update matrix-mock-request

* Make tests happier

* Remove DEBUG/debuglog and use logger.debug

* Update the API to the latest MSC; fixup tests

* Use undefined not null to make it work with the latest changes

* Don't recreate rooms when initial: true

* Add defensive code when unsigned.transaction_id is missing

We can still pair up events by looking at the event_id. We need
to do this in Sliding Sync because the proxy has limitations that
means it cannot guarantee it will always incude a transaction_id
in unsigned. The main reason why is due to the following race condition:
 - A and B are in a DM room.
 - Both are using the proxy.
 - A says "hello".
 - B's sync stream gets "hello" on the proxy. At this point the proxy
   knows it needs to deliver it to A. It does so, but this event has
   no transaction_id as it came down B's sync stream, not A's.
 - If instead, A's sync stream gets "hello" on the proxy, the proxy
   will deliver this message with the transaction_id correctly set.

There are no guarantees that A's sync stream will get the event in a
timely manner, hence the decision to just deliver the events as soon
as the proxy gets the event. This will not be an issue for native
Sliding Sync implementations; this is just a proxy issue.

* Linting

* Add additional sliding sync tests

* Begin adding SlidingSyncSdk tests

* Linting

* Add more sliding sync sdk tests

* Prep work for extension tests

* Linting

* Add account data extension tests

* add to-device tests

* Add E2EE extension tests

* Code smell fixes and extra tests

* Add test for no-txn-id local echo

* Add tests for resolveProfilesToInvites

* Add tests for moving entries down as well as up the list

* Remove conn-management.ts

* Actually verify the event was removed from the txn map

* Handle the case when /sync returns before /send without a txn_id

And ensure all the tests actually test the right things.

* Linting

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-07-12 14:09:58 +00:00
renovate[bot] 7a18991342 Update dependency eslint to v8.19.0 (#2516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-12 13:45:39 +00:00
renovate[bot] f18c64db9e Update typescript-eslint monorepo to v5.30.6 (#2515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-12 13:38:04 +00:00
RiotRobot 7c560b6daa v19.1.0-rc.1 2022-07-12 14:03:09 +01:00
RiotRobot de2add5d5d Prepare changelog for v19.1.0-rc.1 2022-07-12 14:03:08 +01:00
Travis Ralston 24710ee2fc Add a basic PR checklist for all PRs (#2511)
It'll be mildly annoying for core developers who have to constantly remove or edit this, but it'll also serve as a good reminder to do these things.

Note that signoff is not required for core developers.
2022-07-11 14:59:05 -06:00
Šimon Brandner 1fbfdaf221 Don't crash with undefined room in processBeaconEvents() (#2500) 2022-07-11 10:03:44 +02:00
Šimon Brandner c4f7e4d5aa Remove dead code (#2510) 2022-07-11 09:46:50 +02:00
Šimon Brandner 9a6dccb79b Remove setNow from realtime-callbacks.ts (#2509)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-07-10 14:31:48 +02:00
Faye Duxovni 3935152d08 Properly re-insert room ID in bundled thread relation messages from sync (#2505)
Events returned by the `/sync` endpoint, including relations bundled with other events, may have their `room_id`s stripped out. This causes decryption errors if the IDs aren't repopulated.

Fixes vector-im/element-web#22094.
2022-07-08 22:43:38 +00:00
David Baker 984dd26a13 Prevent double mute status changed events (#2502)
Audio & video mute status were set in separate calls but share a
mute status changed event, so you'd always get two mute status
changed events emitted. We could suppress events where the mute
status didn't change, but this would still get two events saying
the same thing when they both changed. Instead, merge setAudioMuted
& setVideoMuted into a single call that sets either or both.
2022-07-08 09:45:02 +01:00
Travis Ralston 72f9a51c27 Actually store the identity server in the client when given as an option (#2503)
* Actually store the identity server in the client when given as an option

* Update requestRegisterEmailToken to a modern spec version too
2022-07-08 01:07:28 -06:00
Travis Ralston efdda8425d Remove MSC3244 support (#2504) 2022-07-08 00:32:27 -06:00
Šimon Brandner 685cab38b9 Improve VoIP integrations testing (#2495) 2022-07-07 08:38:17 +02:00
David Baker bdb91b3806 Set max average bitrate on PTT calls (#2499)
* Set max average bitrate on PTT calls

Via SDP munging. Also makes the SDP munging a bit more generic and
codec-specific (we were previously adding usedtx to any codec that had an fmtp
line already, which was probably not really the intention).

* Make SDP munging for codecs that don't already have fmtp lines

* Use sensible typescript syntax

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-07-06 22:04:41 +01:00
RiotRobot 85a96c6467 Resetting package fields for development 2022-07-05 14:09:11 +01:00
RiotRobot 2f832a9bfe Merge branch 'master' into develop 2022-07-05 14:09:11 +01:00
David Baker 9a15094374 Add config option for e2e group call signalling (#2492) 2022-07-05 13:17:23 +01:00
renovate[bot] f4aecb317f Lock file maintenance (#2491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-04 12:15:41 +00:00
Michael Telatynski ee0264f77d Update pull_request.yaml (#2490) 2022-07-04 10:42:39 +01:00
Robin e980c88901 Don't mute the remote side immediately in PTT calls (#2487)
This clause was causing all PTT calls to mute the remote side
immediately upon ICE connection status changing to 'checking'.
2022-07-02 09:19:07 -04:00
texuf 9bf8b936d4 Fix return type on funcs in matrixClient to be optionally null (#2488) 2022-07-02 09:11:54 +01:00
David Baker 6ea2885796 Remove empty decryption listener (#2486)
* Remove empty decryption listener

This listener looks like it was left over from something as it just
did nothing at all. The todevice event gets put into the call
event buffer which awaits on decryption for each event before
processing, so it should already wait for decryption.

More info: https://github.com/vector-im/element-call/issues/428

* Unused import

* Unused function!
2022-07-01 17:44:00 +01:00
David Baker ca5ac79927 Revert hack to only clone streams on safari (#2485)
Reverts https://github.com/matrix-org/matrix-js-sdk/pull/2450

Looks like this wasn't really the problem (although may have made it
happens faster) and the actual problem was multiple audio contexts
and/or leaking peer connections as fixed in https://github.com/matrix-org/matrix-js-sdk/pull/2484
2022-07-01 17:32:17 +01:00
Robin f9672cf307 Fix some MatrixCall leaks and use a shared AudioContext (#2484)
* Fix some MatrixCall leaks and use a shared AudioContext

These leaks, combined with the dozens of AudioContexts floating around in memory across different CallFeeds, could cause some really bad performance issues and audio crashes on Chrome.

* Fully release the AudioContext in CallFeed's dispose method

* Fix tests
2022-07-01 11:58:00 -04:00
Michael Weimann 9f01c8d1fb Expose KNOWN_SAFE_ROOM_VERSION (#2474) 2022-06-30 08:50:14 +02:00
renovate[bot] df5ab4fa91 Update babel monorepo to v7.18.6 (#2477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-29 16:14:04 -06:00
Robin e7493fd417 Enable DTX on audio tracks in calls (#2482)
This greatly reduces the amount of bandwidth used when transmitting
silence.
2022-06-29 16:06:17 -04:00
David Baker f553854730 Remove the feature to disable audio from muted members (#2479)
At the moment it looks like its more valuable to get the audio from
people even if they're not actually shown as speaking. We can always
re-introduce it later.
2022-06-29 18:47:01 +01:00
David Baker 39465b50cb Go back to forEach in collectcallstats (#2481)
Older typescript library doesn't know about .values() on the stats
object, so it was failing in react sdk which had an older typescript.
https://github.com/matrix-org/matrix-react-sdk/pull/8935 was an
attempt to upgrade it but did not seem to be helping on CI, despite
being fine locally.
2022-06-29 17:33:09 +01:00
David Baker c89bbf4bf5 Fix call.collectCallStats() (#2480)
Regressed by https://github.com/matrix-org/matrix-js-sdk/pull/2352
(you can just use RTCStatsReport as an iterator directly (which
was was what that code was doing before) which uses entries(
which gives you key/value pairs, but using forEach gives you just
the value.
2022-06-29 12:39:36 +01:00
David Baker a745c67dec Fix call.collectCallStats() (#2480)
Regressed by https://github.com/matrix-org/matrix-js-sdk/pull/2352
(you can just use RTCStatsReport as an iterator directly (which
was was what that code was doing before) which uses entries(
which gives you key/value pairs, but using forEach gives you just
the value.
2022-06-29 12:38:48 +01:00
renovate[bot] 55bec4fbe9 Update dependency @types/jest to v28 (#2478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-28 15:53:36 +00:00
renovate[bot] 3a40348860 Update all (#2475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-28 15:52:40 +00:00
renovate[bot] 98262853c7 Update jest monorepo (#2476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-06-28 15:45:36 +00:00
Robin Townsend ebcb26f1b3 Merge branch 'develop' into robertlong/group-call 2022-06-21 11:21:03 -04:00
Robin 5b4263bf55 Don't block muting on determining whether the device exists (#2461)
* Don't block muting on determining whether the device exists

* Add comments
2022-06-16 13:51:32 -04:00
Robin df9ffdc408 Don't ignore call member events with a distant future expiration date (#2466)
to match updates to MSC3401
2022-06-16 12:55:37 -04:00
Robin 70449ea003 Expire call member state events after 1 hour (#2446)
* Expire call member state events after 1 hour

* Fix lints

* Avoid a possible race
2022-06-16 11:31:13 -04:00
David Baker 9192b876d2 Disable playback of audio for muted users (#2456)
* Disable playback of audio for muted users

As hopefully explained by the comment

* forEach instead of map
2022-06-13 20:46:34 +01:00
Travis Ralston 04d0d61a0e Change CODEOWNERS for element-call feature branch (#2457)
To reduce review requests going to the "wrong" team. The team has been mirrored from the vector-im side.
2022-06-13 12:23:50 -06:00
David Baker 404f8e130e Only clone streams on Safari (#2450)
Only enable the stream cloning behaviour on Safari: it was causing
the audio renderer on Chrome (both desktop and Android) to hang,
causing audio to fail sometimes in Element Call and other Chrome
tabs (eg. YouTube) to fail to play audio.

Fixes https://github.com/vector-im/element-call/issues/267
2022-06-10 21:02:04 +01:00
David Baker b97b862fb6 Emit unknown device errors for group call participants without e2e (#2447)
* Emit unknown device errors for group call participants without e2e

There are a number of different cases here: there were some before
when dealing with versions that didn't send deviceId. This catches
all of them and makes all these cases emit the same error.

* Add type
2022-06-10 12:01:40 +01:00
David Baker 5e766978b8 Set PTT mode on call correctly (#2445)
And not always to true. This was causing audio & video to start muted
sometimes on normal calls because the ICE connection state would change
to 'checking', causing the feeds to be muted.
2022-06-08 19:28:53 +01:00
David Baker 34ef7bc64a Mute disconnected peers in PTT mode (#2421)
When we lose ICE connection to peers, set the status of their feeds
to muted so to end their talking session.

For https://github.com/vector-im/element-call/issues/331
2022-05-31 13:43:37 +01:00
David Baker 18e2052af2 Wait for mute event to send in PTT mode (#2401)
This waits until the mute metadata update is sent to all the calls
before telling the user they're unmuted, when in PTT mode (and only
when starting to talk, ie. unmuting). This should help avoid situations
where the signalling connection is slow enough that the unmute event
takes long enough to reach the other side that you hear someone speak
before they've apparently unmuted.

Involves splitting out the method to send the metadata update.
2022-05-26 13:06:57 +01:00
David Baker aa0d3bd1f5 Handle other members having no e2e keys (#2383)
Fetch the device info once at the start of the cal and cache it
rather than fetching every time, and throw if we're supposed to be
using e2e but the other end has no e2e keys.
2022-05-19 19:09:15 +01:00
Robert Long 942a28ddf6 Add support for sending encrypted to-device events with OLM (#2322) 2022-05-18 14:45:26 +01:00
David Baker 87791cd391 Fix races when muting/unmuting (#2370)
await on the async operation so the promise we return resolves once
everything's actuall complete
2022-05-13 19:26:25 +01:00
David Baker 38e54ae7f2 Remove PTT 'other user speaking' logic (#2362)
This was also in Element Call, and whilst js-=sdk might be a more
sensible place, EC has all the information to do it properly (this
impl didn't take admin talk-over into account).
2022-05-11 16:31:14 +01:00
David Baker acef1d7dd0 Merge branch 'dbkr/group-call-merge' of github.com:matrix-org/matrix-js-sdk into dbkr/group-call-merge 2022-05-10 17:24:40 +01:00
David Baker da615fd512 More setTimeout typings 2022-05-10 17:24:21 +01:00
Michael Telatynski f4f05550ef Merge branch 'develop' into dbkr/group-call-merge 2022-05-10 17:11:20 +01:00
David Baker f475251ddd Merge remote-tracking branch 'origin/develop' into dbkr/group-call-merge 2022-05-10 16:50:45 +01:00
David Baker 83f61c96f3 Merge remote-tracking branch 'origin/develop' into dbkr/group-call-merge 2022-05-10 16:39:07 +01:00
David Baker 85a6a552b5 Make tests pass again
Now we know what that bit in the crypto unit test was for...
2022-05-10 16:30:04 +01:00
David Baker 9702e8a5fa Remove test 'fix'
as I can't work out why it was needed, so I can't justify keeping
it in the group calls merge. It should be PRed to develop separately
if needed.
2022-05-09 22:49:32 +01:00
David Baker d82c041b99 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-05-09 22:46:43 +01:00
David Baker 8d9cd0fcb3 Support for PTT group call mode (#2338)
* Add PTT call mode & mute by default in PTT calls (#2311)

No other parts of PTT calls implemented yet

* Make the tests pass again (#2316)

https://github.com/matrix-org/matrix-js-sdk/commit/3280394bf93622c096e3e260296f7f089b97846b
made call use a bunch of methods that weren't mocked in the tests.

* Add maximum trasmit time for PTT (#2312)

on sender side by muting mic after the max transmit time has elapsed.

* Don't allow user to unmute if another user is speaking  (#2313)

* Add maximum trasmit time for PTT

on sender side by muting mic after the max transmit time has elapsed.

* Don't allow user to unmute if another user is speaking

Based on https://github.com/matrix-org/matrix-js-sdk/pull/2312
For https://github.com/vector-im/element-call/issues/298

* Fix createGroupCall arguments (#2325)

Comma instead of a colon...
2022-05-03 13:14:52 +01:00
Robert Long 96ba061732 Fix shouldRequestAudio logging 2022-03-01 16:27:41 -08:00
David Baker ee4cbd1ec9 Don't remove streams that still have tracks (#2104)
If a renogotiation ends up with one track being removed, we removed
the whole stream, which would cause us to lose, for example, audio
rather than just video.
2022-03-01 15:39:36 -08:00
David Baker 2a0dc39eec Fix bug with ine-way audio after a transfer (#2193)
Seems chrome at least will give you a disabled audio track if you
already had another user media audio track and disabled it, so make
sure our tracks are enabled when we add them. We already did this
on one code path but it didn't get moved over when a new code path
was added.

On the plus side, we now know the reason for the ancient code that
had the comment asking what it was for, so update that.
2022-03-01 15:36:48 -08:00
Robert Long 6e25b13312 Send / add end-of-candidates messages 2022-02-28 16:07:36 -08:00
Robert Long 94c5e37570 Fix import 2022-02-25 10:01:17 -08:00
Robert Long 09fee4a2d9 Allow calls to terminate properly when calling stopClient 2022-02-25 09:48:19 -08:00
Robert Long 49994ac4fc Add checks for call/groupCall ended for updateLocalUsermediaStream 2022-02-25 09:42:41 -08:00
Robert Long e68cabc70e Add logging for all stream creation/cloning/muting 2022-02-24 12:55:08 -08:00
Robert Long c819ac634f Fix updating local media streams 2022-02-24 12:10:02 -08:00
Robert Long 17f5ab4191 Move device changes to the application. Add methods to set device ids 2022-02-23 15:07:13 -08:00
Robert Long e270f075a4 Fix call log 2022-02-22 16:57:43 -08:00
Robert Long 0ef6c2e35f Add callId to all logs 2022-02-18 17:47:01 -08:00
Robert Long 7a249e3ef5 Switch media devices on disconnect 2022-02-18 14:35:09 -08:00
Robert Long 353d6bab47 Fix and add a test for toDevice ordering 2022-02-18 11:35:56 -08:00
Robert Long 7f21f569d5 Process toDevice events in order 2022-02-17 14:08:17 -08:00
Robert Long fa5eae70dd Log complete sync errors 2022-02-17 14:07:21 -08:00
David Baker 3db056ad3e Enable max-bundle (#2182)
No particular reason to worry about old user agents here
2022-02-16 11:06:46 -08:00
Robert Long a2a127d9a4 Remove unused isSafari check 2022-02-15 10:53:28 -08:00
Robert Long d12bccd211 Remove safari hack 2022-02-15 10:51:22 -08:00
Robert Long d8e597ccdf Avoid glare 2022-02-11 17:01:22 -08:00
Robert Long c801690e28 Don't reuse local call feeds that have been added to a RTCPeerConnection 2022-02-11 16:56:47 -08:00
Robert Long b4fe00a3a8 Add answer/negotiate response promise chain 2022-02-10 10:31:52 -08:00
Robert Long d42e2fe2c0 Ignore duplicate streams when adding local feeds 2022-02-10 09:32:34 -08:00
Robert Long 4a4465b9fc Don't send candidates after the call has ended 2022-02-07 14:42:07 -08:00
Robert Long 1a78301adb Fix restartIce on FF Android 2022-02-04 12:11:37 -08:00
Robert Long bbf7020755 Remove log 2022-01-18 10:39:02 -08:00
Robert Long 592fb0cf10 Re-enable retries 2022-01-18 10:37:49 -08:00
Robert Long 015eb5d5c4 Add sender/dest session ids 2022-01-14 13:40:42 -08:00
Robert Long 42fef0e7aa Add user id to all send voip events 2022-01-13 14:10:39 -08:00
Robert Long 28f3169a28 Use replace error code when replacing incoming calls 2022-01-12 13:17:19 -08:00
Robert Long d8285aad00 Remove call from callEventHandler after hangup 2022-01-12 13:17:03 -08:00
Robert Long eeacf8c22c Dont filter unstable call events 2022-01-11 17:47:01 -08:00
Robert Long ee995cb39b Ensure call events are processed once and in order 2022-01-11 16:54:40 -08:00
Robert Long 7529af43e4 Add NewSession CallErrorCode 2022-01-11 16:54:12 -08:00
Robert Long 3fac6d7180 Replace outbound calls only 2022-01-10 16:46:55 -08:00
Robert Long 487bfc88ef Merge branch 'robertlong/group-call-session-id' into robertlong/group-call-disable-retries 2022-01-10 16:25:39 -08:00
Robert Long c91617a799 Force hangup replaced calls 2022-01-10 16:22:52 -08:00
Robert Long 87bf115967 Use session ids to resolve refresh during invite/answer 2022-01-10 15:57:40 -08:00
Robert Long 18bb5c3079 Log opponentDeviceId 2022-01-07 11:14:44 -08:00
Robert Long f3f9e41787 Emit sent voip events 2022-01-07 11:14:27 -08:00
Robert Long 7993dd7630 Log opponentDeviceId 2022-01-06 15:46:55 -08:00
Robert Long bef557976b Emit sent voip events 2022-01-06 15:24:59 -08:00
Robert Long 549f9b7e29 Disable retries 2022-01-06 14:44:16 -08:00
Robert Long 06d9d6207c Send device id along with to device signaling messages 2021-11-30 13:39:43 -08:00
Robert Long e336aceaba Expose webrtc related types/props 2021-11-30 13:01:35 -08:00
Robert Long fcc4b71f06 Add LocalStreamsChanged event to MediaHandler 2021-11-29 14:34:43 -08:00
Robert Long d1a62eddfc Set initial audio/video input ids 2021-11-24 12:43:50 -08:00
Robert Long ffbd10a7b8 Make updateLocalUsermediaStreams stop tracks 2021-11-22 13:26:29 -08:00
Robert Long d0e37ee323 Hopefully resolve a race condition with missing device ids 2021-11-19 16:49:20 -08:00
Robert Long 96ef535ebb Make unknown device error more useful 2021-11-19 16:06:29 -08:00
Robert Long 0683133d5b Dont start retry loop until weve sent the member state event 2021-11-19 16:02:26 -08:00
Robert Long 64c3ac55a4 Stop screenshare when screensharing track ended 2021-11-19 13:56:50 -08:00
Robert Long 5f06df8a87 Properly stop screensharing feed 2021-11-18 13:53:30 -08:00
Robert Long 3291846714 Merge branch 'robertlong/abort-sync-error' into robertlong/group-call 2021-11-17 16:10:20 -08:00
Robert Long 139904f297 Update sync state to error when aborting 2021-11-17 16:05:02 -08:00
Robert Long c2fe2ab270 Add additional logging for removing feeds/tracks 2021-11-15 14:20:55 -08:00
Robert Long 4e26f29032 Add unknown device errors 2021-11-15 12:05:34 -08:00
Robert Long 31391121dc Clean up logging 2021-11-15 11:38:47 -08:00
Robert Long 7d48a8394d Don't immediately start retry call loop 2021-11-15 10:48:24 -08:00
Robert Long 28da62c01c Add retry call loop 2021-11-09 15:31:27 -08:00
Robert Long e880cece93 Add restart ICE 2021-11-09 14:40:29 -08:00
Robert Long 97e8fcea75 Clean up replacing calls for Safari 2021-11-09 14:01:16 -08:00
Robert Long f28cb48fe1 Re-enable safari hack 2021-11-08 13:07:22 -08:00
Robert Long a2e255c2c9 Merge branch 'robertlong/group-call' of github.com:matrix-org/matrix-js-sdk into robertlong/group-call 2021-11-08 12:58:54 -08:00
Robert Long 74c5a20371 Temporarily disable safari hack 2021-11-08 12:57:38 -08:00
Robert Long 4b87907b92 Update local usermedia streams serially 2021-11-08 12:30:56 -08:00
Robert Long f76f708c96 Ad a longer wait to safari media stream hack 2021-11-05 14:31:38 -07:00
Robert Long 17f7dc5463 Keep track of original stream id for sdp stream metadata 2021-11-04 17:44:47 -07:00
Robert Long b253ad9e81 Preserve the disabled tracks when updating local usermedia stream 2021-11-04 17:22:37 -07:00
Robert Long c1f56ba3c4 Fix indentation 2021-11-04 11:46:20 -07:00
Robert Long 7998817f7e Send candidate queue again on finish to flush out queue 2021-11-04 11:44:11 -07:00
Robert Long bdc12a2544 Revert changes to gotCallFeedsForAnswer 2021-11-02 17:25:41 -07:00
Robert Long 5a92597abd Check if call ended before getting user media 2021-11-02 15:33:58 -07:00
Robert Long 6f695c1b82 Ignore call call state in glare resolution 2021-11-02 15:30:38 -07:00
Robert Long d99428f2c1 Remove duplicate call answer 2021-11-02 11:39:45 -07:00
Robert Long 4c9648a23b Sanitize call member state 2021-10-28 13:46:27 -07:00
Robert Long 8c5f88c4a7 Fix handling null call 2021-10-28 13:27:35 -07:00
Robert Long 923e9c4ada Ensure that member call state is set correctly 2021-10-28 12:25:00 -07:00
Robert Long 13d62e71b6 Fix stopping all media streams 2021-10-26 16:50:56 -07:00
Robert Long 32aca09f47 Merge branch 'to-device-olm' into robertlong/group-call 2021-10-26 15:27:44 -07:00
Matthew Hodgson 067ac62271 lint 2021-10-26 15:15:30 -07:00
Matthew Hodgson 841e6e999d handle promises normally now tests are fixed 2021-10-26 15:15:16 -07:00
Matthew Hodgson a48546f60d fix the tests (thanks @turt2live!!!) 2021-10-26 15:15:02 -07:00
Matthew Hodgson 2f09e9641c chain promises correctly; log rejects 2021-10-26 15:14:52 -07:00
Matthew Hodgson f46355e7c0 don't choke on missing promise 2021-10-26 15:14:42 -07:00
Matthew Hodgson 53397ee0d1 lint 2021-10-26 15:14:30 -07:00
Matthew Hodgson 5a83635ef5 switch encryptAndSendToDevices to return a promise rather than use a cb
and assert that olm sessions are open to the destination devices
2021-10-26 15:14:13 -07:00
Matthew Hodgson 56c0c9be4d fix example in readme 2021-10-26 15:14:01 -07:00
Matthew Hodgson 24406d2411 make it build 2021-10-26 15:13:46 -07:00
Matthew Hodgson aeeed6ecd7 clarify the factoring 2021-10-26 15:13:27 -07:00
Matthew Hodgson 9f3f9990ef untested first cut at factoring out a encryptAndSendToDevices method 2021-10-26 15:13:13 -07:00
Robert Long 119ce2e46f Fix inbound calls in Safari 2021-10-26 12:44:37 -07:00
Šimon Brandner fc8a867e8e Start processing member state events only after we've set out own (#2000)
This avoids a race condition where the other side would first receive the to-device messages and only then the member state event which would result in the call being ignored

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-25 14:02:28 -07:00
Robert Long b4d8c0b603 Fix updating member state with no existing calls 2021-10-22 11:27:02 -07:00
Robert Long 3b0d1b2696 Add check for existing group call session 2021-10-21 14:23:27 -07:00
Robert Long 5110e0b91e Merge branch 'develop' into robertlong/group-call 2021-10-21 13:10:05 -07:00
Robert Long 305de54106 Fix screensharing and webrtc races 2021-10-21 12:57:30 -07:00
Robert Long 0555f9db1c Only send to device messages to a single device 2021-10-19 17:05:21 -07:00
Robert Long 159e825877 Fix unnecessary param to placeCallWithCallFeeds 2021-10-19 15:31:59 -07:00
Robert Long 8131b3900d Use glare resolution to manage group call setup 2021-10-19 15:30:20 -07:00
Robert Long 431d7a0933 Merge branch 'develop' into robertlong/group-call 2021-10-19 12:37:39 -07:00
Robert Long e9b52e23d2 Rermove session id 2021-10-19 10:57:09 -07:00
Šimon Brandner 0148ad0766 Group call improvements (#1985)
* Add group call events to EventType

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use EventType instead of a const

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make logging around sending group call member state event a bit better

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix m.calls elements being null

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-19 10:25:47 -07:00
Robert Long 213f1134b6 Reduce logging for group calls 2021-10-18 11:49:15 -07:00
Robert Long 50e6a8f6b1 Add session_id check to group calls 2021-10-18 11:49:04 -07:00
Robert Long 4a82e1bf05 Merge pull request #1983 from SimonBrandner/robertlong/group-call
Remove left-over from old screen-sharing code
2021-10-15 16:39:21 -07:00
Šimon Brandner 843973c4da Remove left-over from old screen-sharing code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-15 17:19:22 +02:00
Robert Long debeb66d6f Initialize speakingVolumeSamples for screenshare feeds 2021-10-14 17:24:05 -07:00
Robert Long 015d0f9135 Don't set local user as active speaker 2021-10-14 17:14:12 -07:00
Robert Long 5c8e7f2be0 Improve speaking detection using history 2021-10-14 13:34:20 -07:00
Robert Long 411b5f111c Fix talking indicator 2021-10-13 13:42:40 -07:00
Robert Long 2d231c0ae2 Fix how streams are stopped 2021-10-13 12:05:14 -07:00
Robert Long ec37eb8b6f Add support for switching media devices 2021-10-12 15:20:14 -07:00
Robert Long 1cdcebb5db Merge branch 'robertlong/change-media-device' into robertlong/group-call 2021-10-12 15:15:21 -07:00
Robert Long a0f6eea363 Add support for replacing existing sender tracks 2021-10-12 14:48:59 -07:00
Robert Long 18b1a44df7 Merge branch 'robertlong/change-media-device' into robertlong/group-call 2021-10-12 14:24:02 -07:00
Robert Long 4b6b1599a2 Change media devices mid-call 2021-10-12 14:11:57 -07:00
Robert Long a582b19435 Merge branch 'robertlong/group-call' of github.com:matrix-org/matrix-js-sdk into robertlong/group-call 2021-10-12 12:14:55 -07:00
Robert Long 4a8c3d273f Merge branch 'feature/answer-no-cam' into robertlong/group-call 2021-10-12 12:11:32 -07:00
Robert Long 8dc608d917 Fix connecting to a call without a webcam 2021-10-07 13:47:50 -07:00
Robert Long 7ef38ed1b2 Fix speaking threshold 2021-10-06 16:20:07 -07:00
Robert Long 593f62c1c4 Move to correct event types 2021-10-05 16:51:28 -07:00
Robert Long 04d674b8c7 Merge branch 'SimonBrandner-robertlong/group-call' into robertlong/group-call 2021-10-05 11:42:00 -07:00
Robert Long 27eb88f4a1 Update GroupCall to use new CallFeed constructor 2021-10-05 11:38:03 -07:00
Robert Long 1409a4f814 Merge branch 'develop' into robertlong/group-call 2021-10-05 11:34:55 -07:00
Šimon Brandner 8232896c85 Don't run screen-sharing code for each 1:1 call, share one call feed between them instead
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-05 06:17:01 +02:00
Šimon Brandner e2ed80ffa0 Add removeLocalFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-05 06:17:01 +02:00
Šimon Brandner 8ac3841a2f Handle joining a call after someone has started screen-sharing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-05 06:16:59 +02:00
Robert Long ba57736bf6 Remove log that's stalling FF 2021-10-04 15:43:56 -07:00
Robert Long 8be4ca909e Add participants to GroupCall 2021-10-04 15:36:36 -07:00
Robert Long 0d964523a9 Add screensharing to GroupCall 2021-09-30 16:04:15 -07:00
Robert Long bb504bc001 Handle getUserMedia permissions blocked 2021-09-30 11:39:34 -07:00
Robert Long 326aec9f9e Fix getUserMediaStream 2021-09-29 17:30:34 -07:00
Robert Long 688327dab5 Handle new group call rooms 2021-09-29 17:00:17 -07:00
Robert Long 3f4522ba88 Add more verbose logging for testing. Create group calls on first sync. 2021-09-29 16:22:44 -07:00
Robert Long 625983a2b2 Revert changes to package.json and .npmignore 2021-09-29 14:37:07 -07:00
Robert Long 137fd2bd40 Export group call enums 2021-09-29 14:35:42 -07:00
Robert Long 1e65bfd316 Fix initLocalCallFeed state 2021-09-29 14:35:32 -07:00
Robert Long 5da072712d Use prepack instead of prepare 2021-09-29 11:47:09 -07:00
Robert Long 529d61b5f4 Add .npmignore 2021-09-29 11:44:12 -07:00
Robert Long 5111ca622a Change main path 2021-09-29 11:36:11 -07:00
Robert Long f627507b86 Test prepare script for git installs 2021-09-29 11:03:42 -07:00
Robert Long aee4459201 Merge pull request #1955 from SimonBrandner/robertlong/group-call
De-duplication for group calls
2021-09-28 10:31:55 -07:00
Šimon Brandner 1a824750dd Don't emit the same thing twice
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 19:11:53 +02:00
Šimon Brandner 73cb5e1ee9 Fix order of execution
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 19:10:42 +02:00
Šimon Brandner 96bde1f706 Fix field keys
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 18:17:36 +02:00
Šimon Brandner 5251dcf67f De-duplicate pushNewLocalFeed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 08:58:01 +02:00
Šimon Brandner ce0b0ea182 De-duplicate invite/answer code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 08:57:44 +02:00
Robert Long 7a142e9102 Implement new group call state events 2021-09-27 17:06:09 -07:00
Robert Long f85aa44f28 Remove duplicate FeedChanged event 2021-09-27 15:02:59 -07:00
Robert Long efbf252e22 Merge pull request #1951 from SimonBrandner/robertlong/group-call
Use to-device events for group calls and other improvements
2021-09-27 14:50:39 -07:00
Robert Long d873f14b6d Merge branch 'develop' into robertlong/group-call 2021-09-27 12:10:31 -07:00
Šimon Brandner cf1ba12232 Use arrays of CallFeeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:22 +02:00
Šimon Brandner df208e4de8 Avoid having duplicate call feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:22 +02:00
Šimon Brandner d8ef7f9f63 pushLocalFeed() -> pushNewLocalFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:22 +02:00
Šimon Brandner 2515ba31a0 Use createNewMatrixCall()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:21 +02:00
Šimon Brandner 715c4577d0 Add a prop for not stopping local feeds on end
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:21 +02:00
Šimon Brandner a2f23900c9 Use groupCallId isntead of roomId in to-device messages
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:21 +02:00
Šimon Brandner e9e65cf484 Change type key
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:21 +02:00
Šimon Brandner 205c80ea28 Add groupCallId
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:20 +02:00
Šimon Brandner 678023717b Add a setState() method
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:20 +02:00
Šimon Brandner b535969845 Use to-device messages in group calls
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:20 +02:00
Šimon Brandner 027bc6bfc9 Handle incoming to-device call signalling
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:19 +02:00
Šimon Brandner 71ca424712 Allow for sending toDevice messages
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:19 +02:00
Šimon Brandner 3280394bf9 Figure out opponentMember from the userId rather than the sender
This is because to-device messages don't have a sender

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:19 +02:00
Šimon Brandner fc07530434 Add useToDevice to CallOpts
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:18 +02:00
Robert Long f592d4dbc5 Merge branch 'develop' into robertlong/group-call 2021-09-26 11:30:55 -07:00
Robert Long 96f48929ac Cleaning up group call state 2021-09-24 15:41:05 -07:00
Robert Long 454da84f6e Initialize activeSpeakerSamples 2021-09-24 13:29:23 -07:00
Robert Long 89bda6c2e5 Move from groupCallsParticipants to calls 2021-09-24 12:39:43 -07:00
Robert Long ac70dcfc91 Expose call feed getters on call 2021-09-23 16:23:32 -07:00
Robert Long 9c7cb3cbea Handle more edge cases around creating/ending group calls 2021-09-22 15:03:48 -07:00
Robert Long d8d7bd548f Merge branch 'SimonBrandner-robertlong/group-call' into robertlong/group-call 2021-09-22 12:18:53 -07:00
Šimon Brandner 55ef57ead8 Add GroupCallEventHandler
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-22 21:00:19 +02:00
Šimon Brandner 9996afed03 Throw with no room
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-22 21:00:18 +02:00
Šimon Brandner 61a80a11c9 Export CONF_ROOM
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-22 21:00:18 +02:00
Robert Long 6a8e8ed0a6 Merge branch 'develop' into robertlong/group-call 2021-09-22 11:58:42 -07:00
Robert Long 5895ce32fa Revert unintended babelrc edit 2021-09-21 21:00:47 -07:00
Robert Long fe0a268991 Merge branch 'robertlong/group-call' of github.com:matrix-org/matrix-js-sdk into robertlong/group-call 2021-09-21 17:11:12 -07:00
Robert Long 7f189b0abd Add endCall 2021-09-21 17:10:45 -07:00
Robert Long 6e07c9e900 Clean up group call event listeners properly on hangup 2021-09-21 14:48:22 -07:00
Robert Long bbeea51a36 Add callType to room state event 2021-09-21 14:24:51 -07:00
Robert Long 151b54ed65 Clean up GroupCallParticipant listeners on remove 2021-09-21 14:24:37 -07:00
Robert Long 18986cb33a Fix typo 2021-09-20 17:31:02 -07:00
Robert Long aef5d73de4 Fix emitting participants_changed event 2021-09-20 17:30:49 -07:00
Robert Long e4fc1f3628 Merge branch 'develop' into robertlong/group-call 2021-09-17 09:16:05 -07:00
Robert Long 8b1c173659 Avoid changing member on replaced call 2021-09-16 16:35:41 -07:00
Robert Long f0916f14d1 Merge branch 'develop' into robertlong/group-call 2021-09-15 14:26:13 -07:00
Robert Long a291f5ab05 Merge branch 'robertlong/clone-streams' into robertlong/group-call 2021-09-15 13:23:56 -07:00
Robert Long 2d7e07f4ed Update to use latest datachannel / clone media stream PRs 2021-09-15 12:45:42 -07:00
Robert Long 2427f75f98 Merge branch 'robertlong/datachannels' into robertlong/group-call 2021-09-15 12:40:37 -07:00
Robert Long d25fb71eba Merge branch 'robertlong/clone-streams' into robertlong/group-call 2021-09-15 12:39:50 -07:00
Robert Long c81b9d2fd9 Merge branch 'develop' into robertlong/group-call 2021-09-15 11:27:59 -07:00
Robert Long fb3ca90bc9 Fix private method signatures 2021-09-10 16:06:26 -07:00
Robert Long eb2a47623f Fix active speaker 2021-09-10 15:58:44 -07:00
Robert Long f18d8ead08 Fix usermedia feeds 2021-09-10 14:31:39 -07:00
Robert Long 2da14bd6e9 Fix call feed changed event handler 2021-09-10 10:01:54 -07:00
Robert Long 1dbb776e12 Revert register types 2021-09-09 17:07:18 -07:00
Robert Long 07b2c57064 Remove CallFeed export 2021-09-09 16:40:08 -07:00
Robert Long 7021f70a66 Move from constants to configureable public variables 2021-09-09 16:24:26 -07:00
Robert Long 503e954671 Merge branch 'develop' into robertlong/group-call 2021-09-09 11:27:06 -07:00
Robert Long 2add1fcbcb Clean up group call events 2021-09-08 14:37:21 -07:00
Robert Long 4fe115b2c4 Add initial group call logic 2021-09-08 13:27:38 -07:00
Robert Long 60e168806d Properly dispose of call feeds 2021-09-02 13:27:13 -07:00
Robert Long 03dfab1282 Export CallFeed 2021-09-02 13:01:43 -07:00
Robert Long 19302ea4fb Fix initWithInvitePromise 2021-08-31 16:10:37 -07:00
Robert Long d5aaed67ba Merge branch 'develop' into robertlong/full-mesh-voip 2021-08-31 16:08:02 -07:00
Robert Long 8fe6afd9ab Merge branch 'master' into robertlong/full-mesh-voip 2021-08-31 16:02:05 -07:00
Robert Long 782fbb115f Stop all media on hangup 2021-08-20 14:42:41 -07:00
Robert Long 3971bf34ed Merge branch 'master' into robertlong/full-mesh-voip 2021-08-20 12:09:27 -07:00
Robert Long 6dac6e53f7 Merge branch 'develop' into robertlong/full-mesh-voip 2021-08-11 11:49:52 -07:00
Robert Long 7ec84e92a0 Merge branch 'develop' into robertlong/full-mesh-voip 2021-08-11 11:38:15 -07:00
Robert Long 154e5c45a6 Clear localAVStream when stopping local media stream. 2021-08-09 11:02:09 -07:00
Robert Long 2cd5c813ac Add local media stream functions to client 2021-08-05 18:22:29 -07:00
Robert Long 1c5101aa1a Add ice disconnected timeout 2021-08-04 18:23:21 -07:00
Robert Long 76f11bee9e Fix invitee glare detection and incoming call event 2021-07-26 11:38:18 -07:00
Robert Long 91f409e8f4 Add invitee field 2021-07-21 23:29:08 -07:00
261 changed files with 39247 additions and 16632 deletions
+34 -8
View File
@@ -1,22 +1,32 @@
module.exports = {
plugins: [
"matrix-org",
"import",
],
extends: [
"plugin:matrix-org/babel",
"plugin:import/typescript",
],
env: {
browser: true,
node: true,
},
settings: {
"import/resolver": {
typescript: true,
node: true,
},
},
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
"no-var": ["warn"],
"prefer-rest-params": ["warn"],
"prefer-spread": ["warn"],
"one-var": ["warn"],
"padded-blocks": ["warn"],
"no-extend-native": ["warn"],
"camelcase": ["warn"],
"no-var": ["error"],
"prefer-rest-params": ["error"],
"prefer-spread": ["error"],
"one-var": ["error"],
"padded-blocks": ["error"],
"no-extend-native": ["error"],
"camelcase": ["error"],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"space-before-function-paren": ["error", {
"anonymous": "never",
@@ -33,7 +43,19 @@ module.exports = {
"no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", "events"],
"no-restricted-imports": ["error", {
name: "events",
message: "Please use TypedEventEmitter instead"
}],
"import/no-restricted-paths": ["error", {
"zones": [{
"target": "./src/",
"from": "./src/index.ts",
"message": "The package index is dynamic between src and lib depending on " +
"whether release or development, target the specific module or matrix.ts instead",
}],
}],
},
overrides: [{
files: [
@@ -55,6 +77,10 @@ module.exports = {
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// The non-TypeScript rule produces false positives
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"],
"quotes": "off",
// We use a `logger` intermediary module
"no-console": "error",
+3
View File
@@ -1 +1,4 @@
* @matrix-org/element-web
/src/webrtc @matrix-org/element-call-reviewers
/spec/*/webrtc @matrix-org/element-call-reviewers
+10 -4
View File
@@ -1,7 +1,13 @@
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
## Checklist
<!-- To specify text for the changelog entry (otherwise the PR title will be used):
Notes:
* [ ] Tests written for new code (and old code if feasible)
* [ ] Linter and other CI checks pass
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
<!--
If you would like to specify text for the changelog entry other than your PR title, add the following:
Notes: Add super cool feature
-->
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>matrix-org/renovate-config-element-web"
]
}
+30
View File
@@ -0,0 +1,30 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
branches:
- develop
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: tibdex/backport@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
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+34
View File
@@ -0,0 +1,34 @@
name: Deploy documentation PR preview
on:
workflow_run:
workflows: [ "Static Analysis" ]
types:
- completed
jobs:
netlify:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
# 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@b12b127cf24433d14b4f93cee62f5465076ba82a # v2.24.1
with:
workflow: static_analysis.yml
run_id: ${{ github.event.workflow_run.id }}
name: docs
path: docs
- name: 📤 Deploy to Netlify
uses: matrix-org/netlify-pr-preview@v1
with:
path: docs
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
site_id: ${{ secrets.NETLIFY_SITE_ID }}
desc: Documentation preview
deployment_env: PR Documentation Preview
+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@v1
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
+3 -4
View File
@@ -16,7 +16,6 @@ concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
jobs:
changelog:
name: Preview Changelog
if: github.event.action != 'synchronize'
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
@@ -31,7 +30,7 @@ jobs:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v5
uses: actions/github-script@v6
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
@@ -53,7 +52,7 @@ jobs:
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
uses: actions/github-script@v5
uses: actions/github-script@v6
with:
script: |
github.rest.issues.addLabels({
@@ -72,7 +71,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v5
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
+41
View File
@@ -0,0 +1,41 @@
# Must only be called from `release#published` triggers
name: Publish to npm
on:
workflow_call:
secrets:
NPM_TOKEN:
required: true
jobs:
npm:
name: Publish to npm
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
registry-url: 'https://registry.npmjs.org'
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
tag: next
- name: 🎖️ Add `latest` dist-tag to final releases
if: github.event.release.prerelease == false
run: |
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
env:
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.version }}
@@ -24,11 +24,7 @@ jobs:
- name: 📋 Copy to temp
run: |
ls -lah
tag="${{ github.ref_name }}"
version="${tag#v}"
echo "VERSION=$version" >> $GITHUB_ENV
cp -a "./.jsdoc/matrix-js-sdk/$version" $RUNNER_TEMP
cp -a "./_docs" "$RUNNER_TEMP/"
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v3
@@ -37,10 +33,13 @@ jobs:
- name: 🔪 Prepare
run: |
cp -a "$RUNNER_TEMP/$VERSION" .
tag="${{ github.ref_name }}"
VERSION="${tag#v}"
[ ! -e "$VERSION" ] || rm -r $VERSION
cp -r $RUNNER_TEMP/docs/ $VERSION
# Add the new directory to the index if it isn't there already
if ! grep -q "Version $VERSION" index.html; then
if ! grep -q ">Version $VERSION</a>" index.html; then
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
fi
@@ -51,3 +50,9 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
keep_files: true
publish_dir: .
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+27 -1
View File
@@ -5,13 +5,29 @@ on:
secrets:
SONAR_TOKEN:
required: true
inputs:
extra_args:
type: string
required: false
description: "Extra args to pass to SonarCloud"
jobs:
sonarqube:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
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
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🩻 SonarCloud Scan"
uses: matrix-org/sonarcloud-workflow-action@v2.2
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.3
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
@@ -22,3 +38,13 @@ jobs:
coverage_run_id: ${{ github.event.workflow_run.id }}
coverage_workflow_name: tests.yml
coverage_extract_path: coverage
extra_args: ${{ inputs.extra_args }}
- uses: Sibz/github-status-action@v1
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+28
View File
@@ -8,8 +8,36 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
prepare:
name: Prepare
runs-on: ubuntu-latest
outputs:
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
testExecutionReportPaths: ${{ steps.extra_args.outputs.testExecutionReportPaths }}
steps:
# 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
with:
workflow: tests.yaml
run_id: ${{ github.event.workflow_run.id }}
name: coverage
path: coverage
- id: extra_args
run: |
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
sonarqube:
name: 🩻 SonarQube
needs: prepare
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
+21 -3
View File
@@ -11,7 +11,7 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
@@ -23,11 +23,21 @@ jobs:
- name: Typecheck
run: "yarn run lint:types"
- name: Switch js-sdk to release mode
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Typecheck (release mode)
run: "yarn run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
@@ -43,7 +53,7 @@ jobs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
@@ -54,3 +64,11 @@ jobs:
- name: Generate Docs
run: "yarn run gendoc"
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: docs
path: _docs
# We'll only use this in a workflow_run, then we're done with it
retention-days: 1
+20 -5
View File
@@ -8,28 +8,43 @@ concurrency:
cancel-in-progress: true
jobs:
jest:
name: Jest
name: 'Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})'
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
specs: [browserify, integ, unit]
node: [16, 18, latest]
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Yarn cache
- name: Setup Node
uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: ${{ matrix.node }}
- name: Install dependencies
run: "yarn install"
- name: Build
if: matrix.specs == 'browserify'
run: "yarn build"
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1
- name: Run tests with coverage
run: "yarn coverage --ci --reporters github-actions"
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: Upload Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
+2 -2
View File
@@ -1,5 +1,5 @@
/.jsdocbuild
/.jsdoc
/_docs
.DS_Store
node_modules
/.npmrc
+243
View File
@@ -1,3 +1,246 @@
Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22)
==================================================================================================
## ✨ Features
* Make calls go back to 'connecting' state when media lost ([\#2880](https://github.com/matrix-org/matrix-js-sdk/pull/2880)).
* Add ability to send unthreaded receipt ([\#2878](https://github.com/matrix-org/matrix-js-sdk/pull/2878)).
* Add way to abort search requests ([\#2877](https://github.com/matrix-org/matrix-js-sdk/pull/2877)).
* sliding sync: add custom room subscriptions support ([\#2834](https://github.com/matrix-org/matrix-js-sdk/pull/2834)).
* webrtc: add advanced audio settings ([\#2434](https://github.com/matrix-org/matrix-js-sdk/pull/2434)). Contributed by @MrAnno.
* Add support for group calls using MSC3401 ([\#2553](https://github.com/matrix-org/matrix-js-sdk/pull/2553)).
* Make the js-sdk conform to tsc --strict ([\#2835](https://github.com/matrix-org/matrix-js-sdk/pull/2835)). Fixes #2112 #2116 and #2124.
* Let leave requests outlive the window ([\#2815](https://github.com/matrix-org/matrix-js-sdk/pull/2815)). Fixes vector-im/element-call#639.
* Add event and message capabilities to RoomWidgetClient ([\#2797](https://github.com/matrix-org/matrix-js-sdk/pull/2797)).
* Misc fixes for group call widgets ([\#2657](https://github.com/matrix-org/matrix-js-sdk/pull/2657)).
* Support nested Matrix clients via the widget API ([\#2473](https://github.com/matrix-org/matrix-js-sdk/pull/2473)).
* Set max average bitrate on PTT calls ([\#2499](https://github.com/matrix-org/matrix-js-sdk/pull/2499)). Fixes vector-im/element-call#440.
* Add config option for e2e group call signalling ([\#2492](https://github.com/matrix-org/matrix-js-sdk/pull/2492)).
* Enable DTX on audio tracks in calls ([\#2482](https://github.com/matrix-org/matrix-js-sdk/pull/2482)).
* Don't ignore call member events with a distant future expiration date ([\#2466](https://github.com/matrix-org/matrix-js-sdk/pull/2466)).
* Expire call member state events after 1 hour ([\#2446](https://github.com/matrix-org/matrix-js-sdk/pull/2446)).
* Emit unknown device errors for group call participants without e2e ([\#2447](https://github.com/matrix-org/matrix-js-sdk/pull/2447)).
* Mute disconnected peers in PTT mode ([\#2421](https://github.com/matrix-org/matrix-js-sdk/pull/2421)).
* Add support for sending encrypted to-device events with OLM ([\#2322](https://github.com/matrix-org/matrix-js-sdk/pull/2322)). Contributed by @robertlong.
* Support for PTT group call mode ([\#2338](https://github.com/matrix-org/matrix-js-sdk/pull/2338)).
## 🐛 Bug Fixes
* Fix registration add phone number not working ([\#2876](https://github.com/matrix-org/matrix-js-sdk/pull/2876)). Contributed by @bagvand.
* Use an underride rule for Element Call notifications ([\#2873](https://github.com/matrix-org/matrix-js-sdk/pull/2873)). Fixes vector-im/element-web#23691.
* Fixes unwanted highlight notifications with encrypted threads ([\#2862](https://github.com/matrix-org/matrix-js-sdk/pull/2862)).
* Extra insurance that we don't mix events in the wrong timelines - v2 ([\#2856](https://github.com/matrix-org/matrix-js-sdk/pull/2856)). Contributed by @MadLittleMods.
* Hide pending events in thread timelines ([\#2843](https://github.com/matrix-org/matrix-js-sdk/pull/2843)). Fixes vector-im/element-web#23684.
* Fix pagination token tracking for mixed room timelines ([\#2855](https://github.com/matrix-org/matrix-js-sdk/pull/2855)). Fixes vector-im/element-web#23695.
* Extra insurance that we don't mix events in the wrong timelines ([\#2848](https://github.com/matrix-org/matrix-js-sdk/pull/2848)). Contributed by @MadLittleMods.
* Do not freeze state in `initialiseState()` ([\#2846](https://github.com/matrix-org/matrix-js-sdk/pull/2846)).
* Don't remove our own member for a split second when entering a call ([\#2844](https://github.com/matrix-org/matrix-js-sdk/pull/2844)).
* Resolve races between `initLocalCallFeed` and `leave` ([\#2826](https://github.com/matrix-org/matrix-js-sdk/pull/2826)).
* Add throwOnFail to groupCall.setScreensharingEnabled ([\#2787](https://github.com/matrix-org/matrix-js-sdk/pull/2787)).
* Fix connectivity regressions ([\#2780](https://github.com/matrix-org/matrix-js-sdk/pull/2780)).
* Fix screenshare failing after several attempts ([\#2771](https://github.com/matrix-org/matrix-js-sdk/pull/2771)). Fixes vector-im/element-call#625.
* Don't block muting/unmuting on network requests ([\#2754](https://github.com/matrix-org/matrix-js-sdk/pull/2754)). Fixes vector-im/element-call#592.
* Fix ICE restarts ([\#2702](https://github.com/matrix-org/matrix-js-sdk/pull/2702)).
* Target widget actions at a specific room ([\#2670](https://github.com/matrix-org/matrix-js-sdk/pull/2670)).
* Add tests for ice candidate sending ([\#2674](https://github.com/matrix-org/matrix-js-sdk/pull/2674)).
* Prevent exception when muting ([\#2667](https://github.com/matrix-org/matrix-js-sdk/pull/2667)). Fixes vector-im/element-call#578.
* Fix race in creating calls ([\#2662](https://github.com/matrix-org/matrix-js-sdk/pull/2662)).
* Add client.waitUntilRoomReadyForGroupCalls() ([\#2641](https://github.com/matrix-org/matrix-js-sdk/pull/2641)).
* Wait for client to start syncing before making group calls ([\#2632](https://github.com/matrix-org/matrix-js-sdk/pull/2632)). Fixes #2589.
* Add GroupCallEventHandlerEvent.Room ([\#2631](https://github.com/matrix-org/matrix-js-sdk/pull/2631)).
* Add missing events from reemitter to GroupCall ([\#2527](https://github.com/matrix-org/matrix-js-sdk/pull/2527)). Contributed by @toger5.
* Prevent double mute status changed events ([\#2502](https://github.com/matrix-org/matrix-js-sdk/pull/2502)).
* Don't mute the remote side immediately in PTT calls ([\#2487](https://github.com/matrix-org/matrix-js-sdk/pull/2487)). Fixes vector-im/element-call#425.
* Fix some MatrixCall leaks and use a shared AudioContext ([\#2484](https://github.com/matrix-org/matrix-js-sdk/pull/2484)). Fixes vector-im/element-call#412.
* Don't block muting on determining whether the device exists ([\#2461](https://github.com/matrix-org/matrix-js-sdk/pull/2461)).
* Only clone streams on Safari ([\#2450](https://github.com/matrix-org/matrix-js-sdk/pull/2450)). Fixes vector-im/element-call#267.
* Set PTT mode on call correctly ([\#2445](https://github.com/matrix-org/matrix-js-sdk/pull/2445)). Fixes vector-im/element-call#382.
* Wait for mute event to send in PTT mode ([\#2401](https://github.com/matrix-org/matrix-js-sdk/pull/2401)).
* Handle other members having no e2e keys ([\#2383](https://github.com/matrix-org/matrix-js-sdk/pull/2383)). Fixes vector-im/element-call#338.
* Fix races when muting/unmuting ([\#2370](https://github.com/matrix-org/matrix-js-sdk/pull/2370)).
Changes in [21.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.1.0) (2022-11-08)
==================================================================================================
## ✨ Features
* Loading threads with server-side assistance ([\#2735](https://github.com/matrix-org/matrix-js-sdk/pull/2735)). Contributed by @justjanne.
* Support sign in + E2EE set up using QR code implementing MSC3886, MSC3903 and MSC3906 ([\#2747](https://github.com/matrix-org/matrix-js-sdk/pull/2747)). Contributed by @hughns.
## 🐛 Bug Fixes
* Replace `instanceof Array` with `Array.isArray` ([\#2812](https://github.com/matrix-org/matrix-js-sdk/pull/2812)). Fixes #2811.
* Emit UnreadNotification event on notifications reset ([\#2804](https://github.com/matrix-org/matrix-js-sdk/pull/2804)). Fixes vector-im/element-web#23590.
* Fix incorrect prevEv being sent in ClientEvent.AccountData events ([\#2794](https://github.com/matrix-org/matrix-js-sdk/pull/2794)).
* Fix build error caused by wrong ts-strict improvements ([\#2783](https://github.com/matrix-org/matrix-js-sdk/pull/2783)). Contributed by @justjanne.
* Encryption should not hinder verification ([\#2734](https://github.com/matrix-org/matrix-js-sdk/pull/2734)).
Changes in [21.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.1) (2022-11-01)
==================================================================================================
## 🐛 Bug Fixes
* Fix default behavior of Room.getBlacklistUnverifiedDevices ([\#2830](https://github.com/matrix-org/matrix-js-sdk/pull/2830)). Contributed by @duxovni.
* Catch server versions API call exception when starting the client ([\#2828](https://github.com/matrix-org/matrix-js-sdk/pull/2828)). Fixes vector-im/element-web#23634.
* Fix authedRequest including `Authorization: Bearer undefined` for password resets ([\#2822](https://github.com/matrix-org/matrix-js-sdk/pull/2822)). Fixes vector-im/element-web#23655.
Changes in [21.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.0) (2022-10-25)
==================================================================================================
## 🚨 BREAKING CHANGES
* Changes the `uploadContent` API, kills off `request` and `browser-request` in favour of `fetch`, removed callback support on a lot of the methods, adds a lot of tests. ([\#2719](https://github.com/matrix-org/matrix-js-sdk/pull/2719)). Fixes #2415 and #801.
* Remove deprecated `m.room.aliases` references ([\#2759](https://github.com/matrix-org/matrix-js-sdk/pull/2759)). Fixes vector-im/element-web#12680.
## ✨ Features
* Remove node-specific crypto bits, use Node 16's WebCrypto ([\#2762](https://github.com/matrix-org/matrix-js-sdk/pull/2762)). Fixes #2760.
* Export types for MatrixEvent and Room emitted events, and make event handler map types stricter ([\#2750](https://github.com/matrix-org/matrix-js-sdk/pull/2750)). Contributed by @stas-demydiuk.
* Use even more stable calls to `/room_keys` ([\#2746](https://github.com/matrix-org/matrix-js-sdk/pull/2746)).
* Upgrade to Olm 3.2.13 which has been repackaged to support Node 18 ([\#2744](https://github.com/matrix-org/matrix-js-sdk/pull/2744)).
* Fix `power_level_content_override` type ([\#2741](https://github.com/matrix-org/matrix-js-sdk/pull/2741)).
* Add custom notification handling for MSC3401 call events ([\#2720](https://github.com/matrix-org/matrix-js-sdk/pull/2720)).
* Add support for unread thread notifications ([\#2726](https://github.com/matrix-org/matrix-js-sdk/pull/2726)).
* Load Thread List with server-side assistance (MSC3856) ([\#2602](https://github.com/matrix-org/matrix-js-sdk/pull/2602)).
* Use stable calls to `/room_keys` ([\#2729](https://github.com/matrix-org/matrix-js-sdk/pull/2729)). Fixes vector-im/element-web#22839.
## 🐛 Bug Fixes
* Fix POST data not being passed for registerWithIdentityServer ([\#2769](https://github.com/matrix-org/matrix-js-sdk/pull/2769)). Fixes matrix-org/element-web-rageshakes#16206.
* Fix IdentityPrefix.V2 containing spurious `/api` ([\#2761](https://github.com/matrix-org/matrix-js-sdk/pull/2761)). Fixes vector-im/element-web#23505.
* Always send back an httpStatus property if one is known ([\#2753](https://github.com/matrix-org/matrix-js-sdk/pull/2753)).
* Check for AbortError, not any generic connection error, to avoid tightlooping ([\#2752](https://github.com/matrix-org/matrix-js-sdk/pull/2752)).
* Correct the dir parameter of MSC3715 ([\#2745](https://github.com/matrix-org/matrix-js-sdk/pull/2745)). Contributed by @dhenneke.
* Fix sync init when thread unread notif is not supported ([\#2739](https://github.com/matrix-org/matrix-js-sdk/pull/2739)). Fixes vector-im/element-web#23435.
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
Changes in [20.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0) (2022-10-11)
============================================================================================================
## ✨ Features
* Add local notification settings capability ([\#2700](https://github.com/matrix-org/matrix-js-sdk/pull/2700)).
* Implementation of MSC3882 login token request ([\#2687](https://github.com/matrix-org/matrix-js-sdk/pull/2687)). Contributed by @hughns.
* Typings for MSC2965 OIDC provider discovery ([\#2424](https://github.com/matrix-org/matrix-js-sdk/pull/2424)). Contributed by @hughns.
* Support to remotely toggle push notifications ([\#2686](https://github.com/matrix-org/matrix-js-sdk/pull/2686)).
* Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)).
## 🐛 Bug Fixes
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
* Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee.
* Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377.
* Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784.
Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30)
==================================================================================================
## 🐛 Bug Fixes
* Fix issue in sync when crypto is not supported by client ([\#2715](https://github.com/matrix-org/matrix-js-sdk/pull/2715)). Contributed by @stas-demydiuk.
Changes in [20.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.1) (2022-09-28)
==================================================================================================
## 🐛 Bug Fixes
* Fix missing return when receiving an invitation without shared history ([\#2710](https://github.com/matrix-org/matrix-js-sdk/pull/2710)).
Changes in [20.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.0) (2022-09-28)
==================================================================================================
## 🚨 BREAKING CHANGES
* Bump IDB crypto store version ([\#2705](https://github.com/matrix-org/matrix-js-sdk/pull/2705)).
Changes in [19.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.7.0) (2022-09-28)
==================================================================================================
## 🔒 Security
* Fix for [CVE-2022-39249](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39249)
* Fix for [CVE-2022-39250](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39250)
* Fix for [CVE-2022-39251](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39251)
* Fix for [CVE-2022-39236](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39236)
Changes in [19.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.6.0) (2022-09-27)
==================================================================================================
## ✨ Features
* Add a property aggregating all names of a NamespacedValue ([\#2656](https://github.com/matrix-org/matrix-js-sdk/pull/2656)).
* Implementation of MSC3824 to add action= param on SSO login ([\#2398](https://github.com/matrix-org/matrix-js-sdk/pull/2398)). Contributed by @hughns.
* Add invited_count and joined_count to sliding sync room responses. ([\#2628](https://github.com/matrix-org/matrix-js-sdk/pull/2628)).
* Base support for MSC3847: Ignore invites with policy rooms ([\#2626](https://github.com/matrix-org/matrix-js-sdk/pull/2626)). Contributed by @Yoric.
## 🐛 Bug Fixes
* Fix handling of remote echoes doubling up ([\#2639](https://github.com/matrix-org/matrix-js-sdk/pull/2639)). Fixes #2618.
Changes in [19.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.5.0) (2022-09-13)
==================================================================================================
## 🐛 Bug Fixes
* Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal ([\#2586](https://github.com/matrix-org/matrix-js-sdk/pull/2586)). Contributed by @3nprob.
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
Changes in [19.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.4.0) (2022-08-31)
==================================================================================================
## 🔒 Security
* Fix for [CVE-2022-36059](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D36059)
Find more details at https://matrix.org/blog/2022/08/31/security-releases-matrix-js-sdk-19-4-0-and-matrix-react-sdk-3-53-0
## ✨ Features
* Re-emit room state events on rooms ([\#2607](https://github.com/matrix-org/matrix-js-sdk/pull/2607)).
* Add ability to override built in room name generator for an i18n'able one ([\#2609](https://github.com/matrix-org/matrix-js-sdk/pull/2609)).
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
## 🐛 Bug Fixes
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
* fixed a sliding sync bug which could cause the `roomIndexToRoomId` map to be incorrect when a new room is added in the middle of the list or when an existing room is deleted from the middle of the list. ([\#2610](https://github.com/matrix-org/matrix-js-sdk/pull/2610)).
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078. Contributed by @kerryarchibald.
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
* fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
==================================================================================================
## ✨ Features
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
* Emit an event when the client receives TURN servers ([\#2529](https://github.com/matrix-org/matrix-js-sdk/pull/2529)).
* Add support for stable prefixes for MSC2285 ([\#2524](https://github.com/matrix-org/matrix-js-sdk/pull/2524)).
* Remove stream-replacement ([\#2551](https://github.com/matrix-org/matrix-js-sdk/pull/2551)).
* Add support for sending user-defined encrypted to-device messages ([\#2528](https://github.com/matrix-org/matrix-js-sdk/pull/2528)).
* Retry to-device messages ([\#2549](https://github.com/matrix-org/matrix-js-sdk/pull/2549)). Fixes vector-im/element-web#12851.
* Sliding sync: add missing filters from latest MSC ([\#2555](https://github.com/matrix-org/matrix-js-sdk/pull/2555)).
* Use stable prefixes for MSC3827 ([\#2537](https://github.com/matrix-org/matrix-js-sdk/pull/2537)).
## 🐛 Bug Fixes
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078.
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
* Fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
* Don't load the sync accumulator if there's already a sync persist in flight ([\#2569](https://github.com/matrix-org/matrix-js-sdk/pull/2569)).
Changes in [19.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.2.0) (2022-08-02)
==================================================================================================
## 🦖 Deprecations
* Remove unstable support for `m.room_key.withheld` ([\#2512](https://github.com/matrix-org/matrix-js-sdk/pull/2512)). Fixes #2233.
## ✨ Features
* Sliding sync: add missing filters from latest MSC ([\#2555](https://github.com/matrix-org/matrix-js-sdk/pull/2555)).
* Use stable prefixes for MSC3827 ([\#2537](https://github.com/matrix-org/matrix-js-sdk/pull/2537)).
* Add support for MSC3575: Sliding Sync ([\#2242](https://github.com/matrix-org/matrix-js-sdk/pull/2242)).
## 🐛 Bug Fixes
* Correct the units in TURN servers expiry documentation ([\#2520](https://github.com/matrix-org/matrix-js-sdk/pull/2520)).
* Re-insert room IDs when decrypting bundled redaction events returned by `/sync` ([\#2531](https://github.com/matrix-org/matrix-js-sdk/pull/2531)). Contributed by @duxovni.
Changes in [19.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.1.0) (2022-07-26)
==================================================================================================
## 🦖 Deprecations
* Remove MSC3244 support ([\#2504](https://github.com/matrix-org/matrix-js-sdk/pull/2504)).
## ✨ Features
* `room` now exports `KNOWN_SAFE_ROOM_VERSION` ([\#2474](https://github.com/matrix-org/matrix-js-sdk/pull/2474)).
## 🐛 Bug Fixes
* Don't crash with undefined room in `processBeaconEvents()` ([\#2500](https://github.com/matrix-org/matrix-js-sdk/pull/2500)). Fixes #2494.
* Properly re-insert room ID in bundled thread relation messages from sync ([\#2505](https://github.com/matrix-org/matrix-js-sdk/pull/2505)). Fixes vector-im/element-web#22094. Contributed by @duxovni.
* Actually store the identity server in the client when given as an option ([\#2503](https://github.com/matrix-org/matrix-js-sdk/pull/2503)). Fixes vector-im/element-web#22757.
* Fix call.collectCallStats() ([\#2480](https://github.com/matrix-org/matrix-js-sdk/pull/2480)).
Changes in [19.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.0.0) (2022-07-05)
==================================================================================================
+1 -280
View File
@@ -1,284 +1,5 @@
Contributing code to matrix-js-sdk
==================================
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)).
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
How to contribute
-----------------
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description:
* A changelog entry in the `Notes` section (see below)
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
* Describe the why and what is changing in the PR description so it's easy for
onlookers and reviewers to onboard and context switch. This information is
also helpful when we come back to look at this in 6 months and ask "why did
we do it like that?" we have a chance of finding out.
* Why didn't it work before? Why does it work now? What use cases does it
unlock?
* If you find yourself adding information on how the code works or why you
chose to do it the way you did, make sure this information is instead
written as comments in the code itself.
* Sometimes a PR can change considerably as it is developed. In this case,
the description should be updated to reflect the most recent state of
the PR. (It can be helpful to retain the old content under a suitable
heading, for additional context.)
* Include both **before** and **after** screenshots to easily compare and discuss
what's changing.
* Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change.
* Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it.
We rely on information in pull request to populate the information that goes
into the changelogs our users see, both for the JS SDK itself and also for some
projects based on it. This is picked up from both labels on the pull request and
the `Notes:` annotation in the description. By default, the PR title will be
used for the changelog entry, but you can specify more options, as follows.
To add a longer, more detailed description of the change for the changelog:
*Fix llama herding bug*
```
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
```
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
the default for PRs labelled with `T-Task`):
*Remove outdated comment from `Ungulates.ts`*
```
Notes: none
```
Sometimes, you're fixing a bug in a downstream project, in which case you want
an entry in that project's changelog. You can do that too:
*Fix another herding bug*
```
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
```
This example is for Element Web. You can specify:
* matrix-react-sdk
* element-web
* element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
to specify in the notes that it's a breaking change - this will be added
automatically based on the label - but remember to tell the developer how to
migrate:
*Remove legacy class*
```
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
```
Other metadata can be added using labels.
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
* `T-Defect`: A bug fix (in either code or docs).
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
If you don't have permission to add labels, your PR reviewer(s) can work with you
to add them: ask in the PR description or comments.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Tests
-----
Your PR should include tests.
For new user facing features in `matrix-react-sdk` or `element-web`, you
must include:
1. Comprehensive unit tests written in Jest. These are located in `/test`.
2. "happy path" end-to-end tests.
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
are run using `element-web`. Ideally, you would also include tests for edge
and error cases.
Unit tests are expected even when the feature is in labs. It's good practice
to write tests alongside the code as it ensures the code is testable from
the start, and gives you a fast feedback loop while you're developing the
functionality. End-to-end tests should be added prior to the feature
leaving labs, but don't have to be present from the start (although it might
be beneficial to have some running early, so you can test things faster).
For bugs in those repos, your change must include at least one unit test or
end-to-end test; which is best depends on what sort of test most concisely
exercises the area.
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
These are located in `/spec/`.
When writing unit tests, please aim for a high level of test coverage
for new code - 80% or greater. If you cannot achieve that, please document
why it's not possible in your PR.
Some sections of code are not sensible to add coverage for, such as those
which explicitly inhibit noisy logging for tests. Which can be hidden using
an istanbul magic comment as [documented here][1]. See example:
```javascript
/* istanbul ignore if */
if (process.env.NODE_ENV !== "test") {
logger.error("Log line that is noisy enough in tests to want to skip");
}
```
Tests validate that your change works as intended and also document
concisely what is being changed. Ideally, your new tests fail
prior to your change, and succeed once it has been applied. You may
find this simpler to achieve if you write the tests first.
If you're spiking some code that's experimental and not being used to support
production features, exceptions can be made to requirements for tests.
Note that tests will still be required in order to ship the feature, and it's
strongly encouraged to think about tests early in the process, as adding
tests later will become progressively more difficult.
If you're not sure how to approach writing tests for your change, ask for help
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
Code style
----------
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and ***never*** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
-----------
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
--------
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```
Review expectations
===================
See https://github.com/vector-im/element-meta/wiki/Review-process
Merge Strategy
==============
The preferred method for merging pull requests is squash merging to keep the
commit history trim, but it is up to the discretion of the team member merging
the change. We do not support rebase merges due to `allchange` being unable to
handle them. When merging make sure to leave the default commit title, or
at least leave the PR number at the end in brackets like by default.
When stacking pull requests, you may wish to do the following:
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
[1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md
+5 -7
View File
@@ -33,10 +33,8 @@ In Node.js
----------
Ensure you have the latest LTS version of Node.js installed.
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
a bundler like webpack you'll likely have to transpile dependencies, including this
SDK, to match your target browsers.
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
@@ -297,13 +295,13 @@ API Reference
A hosted reference can be found at
http://matrix-org.github.io/matrix-js-sdk/index.html
This SDK uses JSDoc3 style comments. You can manually build and
This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. You can manually build and
host the API reference from the source files like this:
```
$ yarn gendoc
$ cd .jsdoc
$ python -m SimpleHTTPServer 8005
$ cd _docs
$ python -m http.server 8005
```
Then visit ``http://localhost:8005`` to see the API docs.
-30
View File
@@ -1,30 +0,0 @@
{
"tags": {
"allowUnknownTags": true
},
"plugins": [
"node_modules/better-docs/category",
"node_modules/better-docs/typescript"
],
"source": {
"include": [
"src"
],
"includePattern": ".(ts|js)$"
},
"opts": {
"encoding": "utf8",
"destination": ".jsdoc",
"readme": "README.md",
"recurse": true,
"verbose": true,
"template": "node_modules/docdash"
},
"docdash": {
"static": true,
"private": false,
"search": true,
"collapse": true,
"typedefs": true
}
}
+37 -26
View File
@@ -1,9 +1,9 @@
{
"name": "matrix-js-sdk",
"version": "19.0.0",
"version": "21.2.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=12.9.0"
"node": ">=16.0.0"
},
"scripts": {
"prepublishOnly": "yarn build",
@@ -16,7 +16,7 @@
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -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:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src spec",
"lint:js-fix": "eslint --fix src spec",
@@ -54,15 +54,16 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/sdp-transform": "^2.4.5",
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.7",
"p-retry": "^4.5.0",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.0.0",
"p-retry": "4",
"qs": "^6.9.6",
"request": "^2.88.2",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
@@ -78,34 +79,40 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@casualbot/jest-sonar-reporter": "^2.2.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/jest": "^27.0.0",
"@types/node": "12",
"@types/request": "^2.48.5",
"@types/domexception": "^4.0.0",
"@types/jest": "^29.0.0",
"@types/node": "16",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"babel-jest": "^28.0.0",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "8.16.0",
"domexception": "^4.0.0",
"eslint": "8.26.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-matrix-org": "^0.5.0",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.1.2",
"jest": "^28.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-matrix-org": "^0.7.0",
"eslint-plugin-unicorn": "^44.0.2",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^28.1.3",
"jest-localstorage-mock": "^2.4.6",
"jest-sonar-reporter": "^2.0.0",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^2.0.1",
"jest-mock": "^29.0.0",
"matrix-mock-request": "^2.5.0",
"rimraf": "^3.0.2",
"terser": "^5.5.1",
"tsify": "^5.0.2",
"typedoc": "^0.23.20",
"typedoc-plugin-missing-exports": "^1.0.0",
"typescript": "^4.5.3"
},
"jest": {
@@ -113,6 +120,9 @@
"testMatch": [
"<rootDir>/spec/**/*.spec.{js,ts}"
],
"setupFilesAfterEnv": [
"<rootDir>/spec/setupTests.ts"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts}"
],
@@ -120,11 +130,12 @@
"text-summary",
"lcov"
],
"testResultsProcessor": "jest-sonar-reporter"
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
},
"jestSonar": {
"reportPath": "coverage",
"sonar56x": true
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"typings": "./lib/index.d.ts"
}
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
#
# Script to perform a post-release steps of matrix-js-sdk.
#
# Requires:
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
set -e
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
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
git push origin develop
fi
+81 -94
View File
@@ -3,19 +3,16 @@
# Script to perform a release of matrix-js-sdk and downstream projects.
#
# Requires:
# github-changelog-generator; install via:
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
#
# Note: this script is also used to release matrix-react-sdk and element-web.
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
@@ -26,7 +23,6 @@ else
echo "hub is required: please install it"
exit
fi
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
@@ -37,17 +33,9 @@ $USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
-n: skip publish to NPM
EOF
}
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
if ! git diff-index --quiet --cached HEAD; then
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
exit
@@ -59,10 +47,8 @@ if ! git diff-files --quiet; then
fi
skip_changelog=
skip_npm=
changelog_file="CHANGELOG.md"
expected_npm_user="matrixdotorg"
while getopts hc:u:xzn f; do
while getopts hc:x f; do
case $f in
h)
help
@@ -74,21 +60,70 @@ while getopts hc:u:xzn f; do
x)
skip_changelog=1
;;
n)
skip_npm=1
;;
u)
expected_npm_user="$OPTARG"
;;
esac
done
shift `expr $OPTIND - 1`
shift $(expr $OPTIND - 1)
if [ $# -ne 1 ]; then
echo "Usage: $USAGE" >&2
exit 1
fi
function check_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Checking version of $1..."
local latestver=$(yarn info -s "$1" dist-tags.next)
if [ "$depver" != "$latestver" ]
then
echo "The latest version of $1 is $latestver but package.json depends on $depver."
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
read resp
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
then
echo "Aborting."
exit 1
fi
if [ "$resp" == "u" ]
then
echo "Upgrading $1 to $latestver..."
yarn add -E "$1@$latestver"
git add -u
git commit -m "Upgrade $1 to $latestver"
fi
fi
}
function reset_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Resetting $1 to develop branch..."
yarn add "github:matrix-org/$1#develop"
git add -u
git commit -m "Reset $1 back to develop branch"
}
has_subprojects=0
if [ -f release_config.yaml ]; then
subprojects=$(cat release_config.yaml | python -c "import yaml; import sys; print(' '.join(list(yaml.load(sys.stdin)['subprojects'].keys())))" 2> /dev/null)
if [ "$?" -eq 0 ]; then
has_subprojects=1
echo "Checking subprojects for upgrades"
for proj in $subprojects; do
check_dependency "$proj"
done
fi
fi
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
@@ -97,20 +132,9 @@ yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts --pure-lockfile
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
if [ -z "$skip_npm" ]; then
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
fi
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
prerelease=0
# We check if this build is a prerelease by looking to
@@ -121,20 +145,11 @@ echo $release | grep -q '-' && prerelease=1
if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
else
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
fi
# we might already be on the release branch, in which case, yay
# If we're on any branch starting with 'release', we don't create
# a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
rel_branch=$(git symbolic-ref --short HEAD)
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
@@ -146,8 +161,8 @@ if [ -z "$skip_changelog" ]; then
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
latest_changes=`mktemp`
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
latest_changes=$(mktemp)
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
set -x
@@ -174,7 +189,7 @@ do
done
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
then
pkglock='yarn.lock'
else
@@ -186,7 +201,7 @@ git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
result=$(cat release_config.yaml | python -c "import yaml; import sys; print(yaml.load(sys.stdin)['signing_id'])" 2> /dev/null || true)
if [ "$?" -eq 0 ]; then
signing_id=$result
fi
@@ -204,8 +219,8 @@ assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=`pwd`
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
projdir=$(pwd)
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
@@ -230,7 +245,7 @@ fi
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else
git tag -a -F "${latest_changes}" "$tag"
fi
@@ -296,7 +311,7 @@ if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
release_text=`mktemp`
release_text=$(mktemp)
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
@@ -308,19 +323,6 @@ fi
rm "${release_text}"
rm "${latest_changes}"
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
# Tag both releases and prereleases as `next` so the last stable release remains
# the default.
if [ -z "$skip_npm" ]; then
npm publish --tag next
if [ $prerelease -eq 0 ]; then
# For a release, also add the default `latest` tag.
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
fi
fi
# if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then
git checkout "$rel_branch"
@@ -337,34 +339,19 @@ git merge "$rel_branch" --no-edit
git push origin master
# finally, merge master back onto develop (if it exists)
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
git checkout develop
git pull
git merge master --no-edit
# When merging to develop, we need revert the `main` and `typings` fields if
# we adjusted them previously.
for i in main typings
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
git push origin develop
fi
[ -x ./post-release.sh ] && ./post-release.sh
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
echo "Resetting subprojects to develop"
for proj in $subprojects; do
reset_dependency "$proj"
done
git push origin develop
fi
-16
View File
@@ -1,16 +0,0 @@
{
"extends": [
"config:base",
":dependencyDashboardApproval"
],
"labels": ["T-Task", "Dependencies"],
"lockFileMaintenance": { "enabled": true },
"groupName": "all",
"packageRules": [{
"matchFiles": ["package.json"],
"rangeStrategy": "update-lockfile"
}],
"platformAutomerge": true,
"automerge": true,
"automergeType": "pr"
}
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env node
const fsProm = require('fs/promises');
const PKGJSON = 'package.json';
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
for (const field of ['main', 'typings']) {
if (pkgJson["matrix_lib_"+field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_"+field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
+1 -1
View File
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
sonar.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=spec/**/*
sonar.testExecutionReportPaths=coverage/test-report.xml
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
@@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 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.
@@ -17,31 +17,32 @@ limitations under the License.
/**
* A mock implementation of the webstorage api
* @constructor
*/
export function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
}
export class MockStorageApi {
public data: Record<string, string> = {};
public keys: string[] = [];
public length = 0;
MockStorageApi.prototype = {
setItem: function(k, v) {
public setItem(k: string, v: string): void {
this.data[k] = v;
this._recalc();
},
getItem: function(k) {
this.recalc();
}
public getItem(k: string): string | null {
return this.data[k] || null;
},
removeItem: function(k) {
}
public removeItem(k: string): void {
delete this.data[k];
this._recalc();
},
key: function(index) {
this.recalc();
}
public key(index: number): string {
return this.keys[index];
},
_recalc: function() {
const keys = [];
}
private recalc(): void {
const keys: string[] = [];
for (const k in this.data) {
if (!this.data.hasOwnProperty(k)) {
continue;
@@ -50,6 +51,5 @@ MockStorageApi.prototype = {
}
this.keys = keys;
this.length = keys.length;
},
};
}
}
+19 -16
View File
@@ -30,7 +30,6 @@ import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils";
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
import { IKeyBackupSession } from "../src/crypto/keybackup";
import { IHttpOpts } from "../src/http-api";
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
/**
@@ -39,8 +38,8 @@ import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
export class TestClient {
public readonly httpBackend: MockHttpBackend;
public readonly client: MatrixClient;
private deviceKeys: IDeviceKeys;
private oneTimeKeys: Record<string, IOneTimeKey>;
public deviceKeys?: IDeviceKeys | null;
public oneTimeKeys?: Record<string, IOneTimeKey>;
constructor(
public readonly userId?: string,
@@ -50,17 +49,17 @@ export class TestClient {
options?: Partial<ICreateClientOpts>,
) {
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new MockStorageApi();
sessionStoreBackend = new MockStorageApi() as unknown as Storage;
}
this.httpBackend = new MockHttpBackend();
const fullOptions: ICreateClientOpts = {
baseUrl: "http://" + userId + ".test.server",
baseUrl: "http://" + userId?.slice(1).replace(":", ".") + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
request: this.httpBackend.requestFn as IHttpOpts["request"],
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
...options,
};
if (!fullOptions.cryptoStore) {
@@ -124,7 +123,7 @@ export class TestClient {
logger.log(this + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(this.oneTimeKeys).length).toEqual(0);
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
this.deviceKeys = content.device_keys;
return { one_time_key_counts: { signed_curve25519: 0 } };
@@ -139,9 +138,9 @@ export class TestClient {
* @returns {Promise} for the one-time keys
*/
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
if (Object.keys(this.oneTimeKeys).length != 0) {
if (Object.keys(this.oneTimeKeys!).length != 0) {
// already got one-time keys
return Promise.resolve(this.oneTimeKeys);
return Promise.resolve(this.oneTimeKeys!);
}
this.httpBackend.when("POST", "/keys/upload")
@@ -149,7 +148,7 @@ export class TestClient {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined);
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
} };
});
@@ -159,17 +158,17 @@ export class TestClient {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
logger.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
Object.keys(content.one_time_keys!).length);
this.oneTimeKeys = content.one_time_keys;
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
} };
});
// this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys;
return this.oneTimeKeys!;
});
}
@@ -184,7 +183,7 @@ export class TestClient {
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
200, (_path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual([]);
expect(content.device_keys![userId]).toEqual([]);
});
return response;
});
@@ -207,7 +206,7 @@ export class TestClient {
*/
public getDeviceKey(): string {
const keyId = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
return this.deviceKeys!.keys[keyId];
}
/**
@@ -217,7 +216,7 @@ export class TestClient {
*/
public getSigningKey(): string {
const keyId = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
return this.deviceKeys!.keys[keyId];
}
/**
@@ -236,4 +235,8 @@ export class TestClient {
public isFallbackICEServerAllowed(): boolean {
return true;
}
public getUserId(): string {
return this.userId!;
}
}
+46
View File
@@ -0,0 +1,46 @@
/*
Copyright 2020 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 "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import type { MatrixClient, ClientEvent } from "../../src";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
matrixcs: {
MatrixClient: typeof MatrixClient;
ClientEvent: typeof ClientEvent;
};
}
}
}
// stub for browser-matrix browserify tests
// @ts-ignore
global.XMLHttpRequest = jest.fn();
afterAll(() => {
// clean up XMLHttpRequest mock
// @ts-ignore
global.XMLHttpRequest = undefined;
});
// Akin to spec/setupTests.ts - but that won't affect the browser-matrix bundle
global.matrixcs = {
...global.matrixcs,
timeoutSignal: () => new AbortController().signal,
};
@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// load XmlHttpRequest mock
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import HttpBackend from "matrix-mock-request";
import "./setupTests";// uses browser-matrix instead of the src
import type { MatrixClient } from "../../src";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
@@ -26,34 +25,42 @@ const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
describe("Browserify Test", function() {
let client;
let httpBackend;
let client: MatrixClient;
let httpBackend: HttpBackend;
beforeEach(() => {
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend = new HttpBackend();
client = new global.matrixcs.MatrixClient({
baseUrl: "http://test.server",
userId: USER_ID,
accessToken: ACCESS_TOKEN,
deviceId: DEVICE_ID,
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
client.startClient();
});
afterEach(async () => {
client.stopClient();
httpBackend.stop();
client.http.abort();
httpBackend.verifyNoOutstandingRequests();
httpBackend.verifyNoOutstandingExpectation();
await httpBackend.stop();
});
it("Sync", function() {
const event = utils.mkMembership({
room: ROOM_ID,
mship: "join",
user: "@other_user:server.test",
name: "Displayname",
});
it("Sync", async () => {
const event = {
type: "m.room.member",
room_id: ROOM_ID,
content: {
membership: "join",
name: "Displayname",
},
event_id: "$foobar",
};
const syncData = {
next_batch: "batch1",
@@ -71,11 +78,16 @@ describe("Browserify Test", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
return Promise.race([
httpBackend.flushAllExpected(),
new Promise((_, reject) => {
client.once("sync.unexpectedError", reject);
}),
]);
httpBackend.when("GET", "/sync").respond(200, syncData);
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
const unexpectedErrorFn = jest.fn();
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
client.startClient();
await httpBackend.flushAllExpected();
await syncPromise;
expect(unexpectedErrorFn).not.toHaveBeenCalled();
}, 20000); // additional timeout as this test can take quite a while
});
@@ -122,7 +122,7 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
event_id: '$event_id',
});
return Promise.all([
@@ -290,8 +290,9 @@ describe("DeviceList management:", function() {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list",
0,
);
});
});
@@ -326,8 +327,9 @@ describe("DeviceList management:", function() {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
0,
);
});
});
@@ -362,8 +364,9 @@ describe("DeviceList management:", function() {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
0,
);
});
});
@@ -378,13 +381,15 @@ describe("DeviceList management:", function() {
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
await anotherTestClient.flushSync();
await anotherTestClient.client.crypto.deviceList.saveIfDirty();
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
// @ts-ignore accessing private property
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
0,
);
});
} finally {
-758
View File
@@ -1,758 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 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.
*/
/* This file consists of a set of integration tests which try to simulate
* communication via an Olm-encrypted room between two users, Alice and Bob.
*
* Note that megolm (group) conversation is not tested here.
*
* See also `megolm.spec.js`.
*/
// load olm before the sdk if possible
import '../olm-loader';
import { logger } from '../../src/logger';
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED } from "../../src/client";
let aliTestClient;
const roomId = "!room:localhost";
const aliUserId = "@ali:localhost";
const aliDeviceId = "zxcvb";
const aliAccessToken = "aseukfgwef";
let bobTestClient;
const bobUserId = "@bob:localhost";
const bobDeviceId = "bvcxz";
const bobAccessToken = "fewgfkuesa";
let aliMessages;
let bobMessages;
function bobUploadsDeviceKeys() {
bobTestClient.expectDeviceKeyUpload();
return Promise.all([
bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flush(),
]).then(() => {
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
});
}
/**
* Set an expectation that ali will query bobs keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliQueryKeys() {
// can't query keys before bob has uploaded them
expect(bobTestClient.deviceKeys).toBeTruthy();
const bobKeys = {};
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
aliTestClient.httpBackend.when("POST", "/keys/query")
.respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual(
[],
"Expected Alice to key query for " + bobUserId + ", got " +
Object.keys(content.device_keys),
);
const result = {};
result[bobUserId] = bobKeys;
return { device_keys: result };
});
return aliTestClient.httpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that bob will query alis keys; then flush the http request.
*
* @return {promise} which resolves once the http request has completed.
*/
function expectBobQueryKeys() {
// can't query keys before ali has uploaded them
expect(aliTestClient.deviceKeys).toBeTruthy();
const aliKeys = {};
aliKeys[aliDeviceId] = aliTestClient.deviceKeys;
logger.log("query result will be", aliKeys);
bobTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual(
[],
"Expected Bob to key query for " + aliUserId + ", got " +
Object.keys(content.device_keys),
);
const result = {};
result[aliUserId] = aliKeys;
return { device_keys: result };
});
return bobTestClient.httpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliClaimKeys() {
return bobTestClient.awaitOneTimeKeyUpload().then((keys) => {
aliTestClient.httpBackend.when(
"POST", "/keys/claim",
).respond(200, function(path, content) {
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = null;
for (keyId in keys) {
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
break;
}
}
}
const result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
return { one_time_keys: result };
});
}).then(() => {
// it can take a while to process the key query, so give it some extra
// time, and make sure the claim actually happens rather than ploughing on
// confusingly.
return aliTestClient.httpBackend.flush("/keys/claim", 1, 500).then((r) => {
expect(r).toEqual(1, "Ali did not claim Bob's keys");
});
});
}
function aliDownloadsKeys() {
// can't query keys before bob has uploaded them
expect(bobTestClient.getSigningKey()).toBeTruthy();
const p1 = aliTestClient.client.downloadKeys([bobUserId]).then(function() {
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
}).then((devices) => {
expect(devices.length).toEqual(1);
expect(devices[0].deviceId).toEqual("bvcxz");
});
const p2 = expectAliQueryKeys();
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
return Promise.all([p1, p2]).then(() => {
return aliTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data.devices[bobUserId];
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
});
});
}
function aliEnablesEncryption() {
return aliTestClient.client.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(aliTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
});
}
function bobEnablesEncryption() {
return bobTestClient.client.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(bobTestClient.client.isRoomEncrypted(roomId)).toBeTruthy();
});
}
/**
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsFirstMessage() {
return Promise.all([
sendMessage(aliTestClient.client),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest),
]).then(function([_, ciphertext]) {
return ciphertext;
});
}
/**
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsMessage() {
return Promise.all([
sendMessage(aliTestClient.client),
expectAliSendMessageRequest(),
]).then(function([_, ciphertext]) {
return ciphertext;
});
}
/**
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
*/
function bobSendsReplyMessage() {
return Promise.all([
sendMessage(bobTestClient.client),
expectBobQueryKeys()
.then(expectBobSendMessageRequest),
]).then(function([_, ciphertext]) {
return ciphertext;
});
}
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectAliSendMessageRequest() {
return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) {
aliMessages.push(content);
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
expect(ciphertext).toBeTruthy();
return ciphertext;
});
}
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectBobSendMessageRequest() {
return expectSendMessageRequest(bobTestClient.httpBackend).then(function(content) {
bobMessages.push(content);
const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeTruthy();
return ciphertext;
});
}
function sendMessage(client) {
return client.sendMessage(
roomId, { msgtype: "m.text", body: "Hello, World" },
);
}
function expectSendMessageRequest(httpBackend) {
const path = "/send/m.room.encrypted/";
const prom = new Promise((resolve) => {
httpBackend.when("PUT", path).respond(200, function(path, content) {
resolve(content);
return {
event_id: "asdfgh",
};
});
});
// it can take a while to process the key query
return httpBackend.flush(path, 1).then(() => prom);
}
function aliRecvMessage() {
const message = bobMessages.shift();
return recvMessage(
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
);
}
function bobRecvMessage() {
const message = aliMessages.shift();
return recvMessage(
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
);
}
function recvMessage(httpBackend, client, sender, message) {
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise((resolve, reject) => {
const onEvent = function(event) {
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
logger.log(client.credentials.userId + " received event",
event);
client.removeListener("event", onEvent);
resolve(event);
};
client.on("event", onEvent);
});
httpBackend.flush();
return eventPromise.then((event) => {
expect(event.isEncrypted()).toBeTruthy();
// it may still be being decrypted
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toMatchObject({
msgtype: "m.text",
body: "Hello, World",
});
expect(event.isEncrypted()).toBeTruthy();
});
}
/**
* Send an initial sync response to the client (which just includes the member
* list for our test room).
*
* @param {TestClient} testClient
* @returns {Promise} which resolves when the sync has been flushed.
*/
function firstSync(testClient) {
// send a sync response including our test room.
const syncData = {
next_batch: "x",
rooms: {
join: { },
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
},
};
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
return testClient.flushSync();
}
describe("MatrixClient crypto", function() {
if (!CRYPTO_ENABLED) {
return;
}
beforeEach(async function() {
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
await bobTestClient.client.initCrypto();
aliMessages = [];
bobMessages = [];
});
afterEach(function() {
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
});
it("Bob uploads device keys", function() {
return Promise.resolve()
.then(bobUploadsDeviceKeys);
});
it("Ali downloads Bobs device keys", function() {
return Promise.resolve()
.then(bobUploadsDeviceKeys)
.then(aliDownloadsKeys);
});
it("Ali gets keys with an invalid signature", function() {
return Promise.resolve()
.then(bobUploadsDeviceKeys)
.then(function() {
// tamper bob's keys
const bobDeviceKeys = bobTestClient.deviceKeys;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
return Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
expectAliQueryKeys(),
]);
}).then(function() {
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
}).then((devices) => {
// should get an empty list
expect(devices).toEqual([]);
});
});
it("Ali gets keys with an incorrect userId", function() {
const eveUserId = "@eve:localhost";
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bvcxz',
keys: {
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
},
user_id: '@eve:localhost',
signatures: {
'@eve:localhost': {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, function(path, content) {
const result = {};
result[bobUserId] = bobKeys;
return { device_keys: result };
});
return Promise.all([
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]).then(function() {
return Promise.all([
aliTestClient.client.getStoredDevicesForUser(bobUserId),
aliTestClient.client.getStoredDevicesForUser(eveUserId),
]);
}).then(([bobDevices, eveDevices]) => {
// should get an empty list
expect(bobDevices).toEqual([]);
expect(eveDevices).toEqual([]);
});
});
it("Ali gets keys with an incorrect deviceId", function() {
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bad_device',
keys: {
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
},
user_id: '@bob:localhost',
signatures: {
'@bob:localhost': {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, function(path, content) {
const result = {};
result[bobUserId] = bobKeys;
return { device_keys: result };
});
return Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]).then(function() {
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
}).then((devices) => {
// should get an empty list
expect(devices).toEqual([]);
});
});
it("Bob starts his client and uploads device keys and one-time keys", function() {
return Promise.resolve()
.then(() => bobTestClient.start())
.then(() => bobTestClient.awaitOneTimeKeyUpload())
.then((keys) => {
expect(Object.keys(keys).length).toEqual(5);
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
});
});
it("Ali sends a message", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage);
});
it("Bob receives a message", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobRecvMessage);
});
it("Bob receives a message with a bogus sender", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(function() {
const message = aliMessages.shift();
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
};
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise((resolve, reject) => {
const onEvent = function(event) {
logger.log(bobUserId + " received event",
event);
resolve(event);
};
bobTestClient.client.once("event", onEvent);
});
bobTestClient.httpBackend.flush();
return eventPromise;
}).then((event) => {
expect(event.isEncrypted()).toBeTruthy();
// it may still be being decrypted
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
});
});
it("Ali blocks Bob's device", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliDownloadsKeys)
.then(function() {
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
const p1 = sendMessage(aliTestClient.client);
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
.then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
return Promise.all([p1, p2]);
});
});
it("Bob receives two pre-key messages", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobRecvMessage)
.then(aliSendsMessage)
.then(bobRecvMessage);
});
it("Bob replies to the message", function() {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(() => firstSync(bobTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobRecvMessage)
.then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1, "Unexpected cipghertext type.");
}).then(aliRecvMessage);
});
it("Ali does a key query when encryption is enabled", function() {
// enabling encryption in the room should make alice download devices
// for both members.
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => firstSync(aliTestClient))
.then(() => {
const syncData = {
next_batch: '2',
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
},
}),
],
},
};
aliTestClient.httpBackend.when('GET', '/sync').respond(
200, syncData);
return aliTestClient.httpBackend.flush('/sync', 1);
}).then(() => {
aliTestClient.expectKeyQuery({
device_keys: {
[bobUserId]: {},
},
});
return aliTestClient.httpBackend.flushAllExpected();
});
});
it("Upload new oneTimeKeys based on a /sync request - no count-asking", function() {
// Send a response which causes a key upload
const httpBackend = aliTestClient.httpBackend;
const syncDataEmpty = {
next_batch: "a",
device_one_time_keys_count: {
signed_curve25519: 0,
},
};
// enqueue expectations:
// * Sync with empty one_time_keys => upload keys
return Promise.resolve()
.then(() => {
logger.log(aliTestClient + ': starting');
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
aliTestClient.client.startClient({});
return httpBackend.flushAllExpected().then(() => {
logger.log(aliTestClient + ': started');
});
})
.then(() => httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
expect(Object.keys(content.one_time_keys).length)
.toBeGreaterThanOrEqual(1);
logger.log('received %i one-time keys',
Object.keys(content.one_time_keys).length);
// cancel futher calls by telling the client
// we have more than we need
return {
one_time_key_counts: {
signed_curve25519: 70,
},
};
}))
.then(() => httpBackend.flushAllExpected());
});
});
+682
View File
@@ -0,0 +1,682 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 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.
*/
/* This file consists of a set of integration tests which try to simulate
* communication via an Olm-encrypted room between two users, Alice and Bob.
*
* Note that megolm (group) conversation is not tested here.
*
* See also `megolm.spec.js`.
*/
// load olm before the sdk if possible
import '../olm-loader';
import { logger } from '../../src/logger';
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from '../../src/crypto/deviceinfo';
let aliTestClient: TestClient;
const roomId = "!room:localhost";
const aliUserId = "@ali:localhost";
const aliDeviceId = "zxcvb";
const aliAccessToken = "aseukfgwef";
let bobTestClient: TestClient;
const bobUserId = "@bob:localhost";
const bobDeviceId = "bvcxz";
const bobAccessToken = "fewgfkuesa";
let aliMessages: IContent[];
let bobMessages: IContent[];
// IMessage isn't exported by src/crypto/algorithms/olm.ts
interface OlmPayload {
type: number;
body: string;
}
async function bobUploadsDeviceKeys(): Promise<void> {
bobTestClient.expectDeviceKeyUpload();
await Promise.all([
bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flushAllExpected(),
]);
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
}
/**
* Set an expectation that querier will query uploader's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
// can't query keys before bob has uploaded them
expect(uploader.deviceKeys).toBeTruthy();
const uploaderKeys = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
querier.httpBackend.when("POST", "/keys/query")
.respond(200, function(_path, content: IUploadKeysRequest) {
expect(content.device_keys![uploader.userId!]).toEqual([]);
const result = {};
result[uploader.userId!] = uploaderKeys;
return { device_keys: result };
});
return querier.httpBackend.flush("/keys/query", 1);
}
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
async function expectAliClaimKeys(): Promise<void> {
const keys = await bobTestClient.awaitOneTimeKeyUpload();
aliTestClient.httpBackend.when(
"POST", "/keys/claim",
).respond(200, function(_path, content: IUploadKeysRequest) {
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = '';
for (keyId in keys) {
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
break;
}
}
}
const result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
return { one_time_keys: result };
});
// it can take a while to process the key query, so give it some extra
// time, and make sure the claim actually happens rather than ploughing on
// confusingly.
const r = await aliTestClient.httpBackend.flush("/keys/claim", 1, 500);
expect(r).toEqual(1);
}
async function aliDownloadsKeys(): Promise<void> {
// can't query keys before bob has uploaded them
expect(bobTestClient.getSigningKey()).toBeTruthy();
const p1 = async () => {
await aliTestClient.client.downloadKeys([bobUserId]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
expect(devices.length).toEqual(1);
expect(devices[0].deviceId).toEqual("bvcxz");
};
const p2 = expectAliQueryKeys;
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
await Promise.all([p1(), p2()]);
await aliTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore - protected
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data!.devices[bobUserId]!;
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
expect(devices[bobDeviceId].verified).
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
});
}
async function clientEnablesEncryption(client: MatrixClient): Promise<void> {
await client.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
});
expect(client.isRoomEncrypted(roomId)).toBeTruthy();
}
const aliEnablesEncryption = () => clientEnablesEncryption(aliTestClient.client);
const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client);
/**
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
async function aliSendsFirstMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(aliTestClient.client),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest),
]);
return ciphertext;
}
/**
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
async function aliSendsMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(aliTestClient.client),
expectAliSendMessageRequest(),
]);
return ciphertext;
}
/**
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
*/
async function bobSendsReplyMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(bobTestClient.client),
expectBobQueryKeys()
.then(expectBobSendMessageRequest),
]);
return ciphertext;
}
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
aliMessages.push(content);
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
expect(ciphertext).toBeTruthy();
return ciphertext;
}
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
bobMessages.push(content);
const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliTestClient.deviceKeys!.keys[aliKeyId];
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeTruthy();
return ciphertext;
}
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
return client.sendMessage(
roomId, { msgtype: "m.text", body: "Hello, World" },
);
}
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
const path = "/send/m.room.encrypted/";
const prom = new Promise<IContent>((resolve) => {
httpBackend.when("PUT", path).respond(200, function(_path, content) {
resolve(content);
return {
event_id: "asdfgh",
};
});
});
// it can take a while to process the key query
await httpBackend.flush(path, 1);
return prom;
}
function aliRecvMessage(): Promise<void> {
const message = bobMessages.shift()!;
return recvMessage(
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
);
}
function bobRecvMessage(): Promise<void> {
const message = aliMessages.shift()!;
return recvMessage(
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
);
}
async function recvMessage(
httpBackend: TestClient["httpBackend"],
client: MatrixClient,
sender: string,
message: IContent,
): Promise<void> {
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function(event: MatrixEvent) {
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
logger.log(client.credentials.userId + " received event",
event);
client.removeListener(ClientEvent.Event, onEvent);
resolve(event);
};
client.on(ClientEvent.Event, onEvent);
});
await httpBackend.flushAllExpected();
const preDecryptionEvent = await eventPromise;
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
// it may still be being decrypted
const event = await testUtils.awaitDecryption(preDecryptionEvent);
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toMatchObject({
msgtype: "m.text",
body: "Hello, World",
});
expect(event.isEncrypted()).toBeTruthy();
}
/**
* Send an initial sync response to the client (which just includes the member
* list for our test room).
*
* @param {TestClient} testClient
* @returns {Promise} which resolves when the sync has been flushed.
*/
function firstSync(testClient: TestClient): Promise<void> {
// send a sync response including our test room.
const syncData = {
next_batch: "x",
rooms: {
join: { },
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
},
};
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
return testClient.flushSync();
}
describe("MatrixClient crypto", () => {
if (!CRYPTO_ENABLED) {
return;
}
beforeEach(async () => {
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
await bobTestClient.client.initCrypto();
aliMessages = [];
bobMessages = [];
});
afterEach(() => {
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
});
it("Bob uploads device keys", bobUploadsDeviceKeys);
it("Ali downloads Bobs device keys", async () => {
await bobUploadsDeviceKeys();
await aliDownloadsKeys();
});
it("Ali gets keys with an invalid signature", async () => {
await bobUploadsDeviceKeys();
// tamper bob's keys
const bobDeviceKeys = bobTestClient.deviceKeys!;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
expectAliQueryKeys(),
]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
// should get an empty list
expect(devices).toEqual([]);
});
it("Ali gets keys with an incorrect userId", async () => {
const eveUserId = "@eve:localhost";
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bvcxz',
keys: {
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
},
user_id: '@eve:localhost',
signatures: {
'@eve:localhost': {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]);
const [bobDevices, eveDevices] = await Promise.all([
aliTestClient.client.getStoredDevicesForUser(bobUserId),
aliTestClient.client.getStoredDevicesForUser(eveUserId),
]);
// should get an empty list
expect(bobDevices).toEqual([]);
expect(eveDevices).toEqual([]);
});
it("Ali gets keys with an incorrect deviceId", async () => {
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bad_device',
keys: {
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
},
user_id: '@bob:localhost',
signatures: {
'@bob:localhost': {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
// should get an empty list
expect(devices).toEqual([]);
});
it("Bob starts his client and uploads device keys and one-time keys", async () => {
await bobTestClient.start();
const keys = await bobTestClient.awaitOneTimeKeyUpload();
expect(Object.keys(keys).length).toEqual(5);
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
});
it("Ali sends a message", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
});
it("Bob receives a message", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
await bobRecvMessage();
});
it("Bob receives a message with a bogus sender", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
const message = aliMessages.shift()!;
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
};
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function(event: MatrixEvent) {
logger.log(bobUserId + " received event", event);
resolve(event);
};
bobTestClient.client.once(ClientEvent.Event, onEvent);
});
await bobTestClient.httpBackend.flushAllExpected();
const preDecryptionEvent = await eventPromise;
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
// it may still be being decrypted
const event = await testUtils.awaitDecryption(preDecryptionEvent);
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
});
it("Ali blocks Bob's device", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliDownloadsKeys();
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
const p1 = sendMessage(aliTestClient.client);
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
.then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
await Promise.all([p1, p2]);
});
it("Bob receives two pre-key messages", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
await bobRecvMessage();
await aliSendsMessage();
await bobRecvMessage();
});
it("Bob replies to the message", async () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
await firstSync(aliTestClient);
await firstSync(bobTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {},
);
await bobRecvMessage();
await bobEnablesEncryption();
const ciphertext = await bobSendsReplyMessage();
expect(ciphertext.type).toEqual(1);
await aliRecvMessage();
});
it("Ali does a key query when encryption is enabled", async () => {
// enabling encryption in the room should make alice download devices
// for both members.
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await firstSync(aliTestClient);
const syncData = {
next_batch: '2',
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
},
}),
],
},
};
aliTestClient.httpBackend.when('GET', '/sync').respond(
200, syncData);
await aliTestClient.httpBackend.flush('/sync', 1);
aliTestClient.expectKeyQuery({
device_keys: {
[bobUserId]: {},
},
failures: {},
});
await aliTestClient.httpBackend.flushAllExpected();
});
it("Upload new oneTimeKeys based on a /sync request - no count-asking", async () => {
// Send a response which causes a key upload
const httpBackend = aliTestClient.httpBackend;
const syncDataEmpty = {
next_batch: "a",
device_one_time_keys_count: {
signed_curve25519: 0,
},
};
// enqueue expectations:
// * Sync with empty one_time_keys => upload keys
logger.log(aliTestClient + ': starting');
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
await Promise.all([
aliTestClient.client.startClient({}),
httpBackend.flushAllExpected(),
]);
logger.log(aliTestClient + ': started');
httpBackend.when("POST", "/keys/upload")
.respond(200, (_path, content: IUploadKeysRequest) => {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
// cancel futher calls by telling the client
// we have more than we need
return {
one_time_key_counts: {
signed_curve25519: 70,
},
};
});
await httpBackend.flushAllExpected();
});
});
@@ -1,25 +1,59 @@
/*
Copyright 2022 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 HttpBackend from "matrix-mock-request";
import {
ClientEvent,
HttpApiEvent,
IEvent,
MatrixClient,
RoomEvent,
RoomMemberEvent,
RoomStateEvent,
UserEvent,
} from "../../src";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient events", function() {
let client;
let httpBackend;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const setupTests = (): [MatrixClient, HttpBackend] => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
const client = testClient.client;
const httpBackend = testClient.httpBackend;
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(function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
[client!, httpBackend] = setupTests();
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
httpBackend?.verifyNoOutstandingExpectation();
client?.stopClient();
return httpBackend?.stop();
});
describe("emissions", function() {
@@ -92,53 +126,49 @@ describe("MatrixClient events", function() {
};
it("should emit events from both the first and subsequent /sync calls",
function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
function() {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let expectedEvents = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
let expectedEvents: Partial<IEvent>[] = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
client.on("event", function(event) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
client!.on(ClientEvent.Event, function(event) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
}
}
}
expect(found).toBe(
true, "Unexpected 'event' emitted: " + event.getType(),
);
});
expect(found).toBe(true);
});
client.startClient();
client!.startClient();
return Promise.all([
return Promise.all([
// wait for two SYNCING events
utils.syncPromise(client).then(() => {
return utils.syncPromise(client);
}),
httpBackend.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(
0, "Failed to see all events from /sync calls",
);
utils.syncPromise(client!).then(() => {
return utils.syncPromise(client!);
}),
httpBackend!.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(0);
});
});
});
it("should emit User events", function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let fired = false;
client.on("User.presence", function(event, user) {
client!.on(UserEvent.Presence, function(event, user) {
fired = true;
expect(user).toBeTruthy();
expect(event).toBeTruthy();
@@ -146,58 +176,52 @@ describe("MatrixClient events", function() {
return;
}
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
SYNC_DATA.presence.events[0].content.presence,
SYNC_DATA.presence.events[0]?.content?.presence,
);
});
client.startClient();
client!.startClient();
httpBackend.flushAllExpected().then(function() {
expect(fired).toBe(true, "User.presence didn't fire.");
httpBackend!.flushAllExpected().then(function() {
expect(fired).toBe(true);
done();
});
});
it("should emit Room events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let roomInvokeCount = 0;
let roomNameInvokeCount = 0;
let timelineFireCount = 0;
client.on("Room", function(room) {
client!.on(ClientEvent.Room, function(room) {
roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar");
});
client.on("Room.timeline", function(event, room) {
client!.on(RoomEvent.Timeline, function(event, room) {
timelineFireCount++;
expect(room.roomId).toEqual("!erufh:bar");
expect(room?.roomId).toEqual("!erufh:bar");
});
client.on("Room.name", function(room) {
client!.on(RoomEvent.Name, function(room) {
roomNameInvokeCount++;
});
client.startClient();
client!.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(roomInvokeCount).toEqual(
1, "Room fired wrong number of times.",
);
expect(roomNameInvokeCount).toEqual(
1, "Room.name fired wrong number of times.",
);
expect(timelineFireCount).toEqual(
3, "Room.timeline fired the wrong number of times",
);
expect(roomInvokeCount).toEqual(1);
expect(roomNameInvokeCount).toEqual(1);
expect(timelineFireCount).toEqual(3);
});
});
it("should emit RoomState events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
const roomStateEventTypes = [
"m.room.member", "m.room.create",
@@ -205,126 +229,106 @@ describe("MatrixClient events", function() {
let eventsInvokeCount = 0;
let membersInvokeCount = 0;
let newMemberInvokeCount = 0;
client.on("RoomState.events", function(event, state) {
client!.on(RoomStateEvent.Events, function(event, state) {
eventsInvokeCount++;
const index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
-1, "Unexpected room state event type: " + event.getType(),
);
expect(index).not.toEqual(-1);
if (index >= 0) {
roomStateEventTypes.splice(index, 1);
}
});
client.on("RoomState.members", function(event, state, member) {
client!.on(RoomStateEvent.Members, function(event, state, member) {
membersInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toEqual("join");
});
client.on("RoomState.newMember", function(event, state, member) {
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
newMemberInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toBeFalsy();
});
client.startClient();
client!.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(membersInvokeCount).toEqual(
1, "RoomState.members fired wrong number of times",
);
expect(newMemberInvokeCount).toEqual(
1, "RoomState.newMember fired wrong number of times",
);
expect(eventsInvokeCount).toEqual(
2, "RoomState.events fired wrong number of times",
);
expect(membersInvokeCount).toEqual(1);
expect(newMemberInvokeCount).toEqual(1);
expect(eventsInvokeCount).toEqual(2);
});
});
it("should emit RoomMember events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let typingInvokeCount = 0;
let powerLevelInvokeCount = 0;
let nameInvokeCount = 0;
let membershipInvokeCount = 0;
client.on("RoomMember.name", function(event, member) {
client!.on(RoomMemberEvent.Name, function(event, member) {
nameInvokeCount++;
});
client.on("RoomMember.typing", function(event, member) {
client!.on(RoomMemberEvent.Typing, function(event, member) {
typingInvokeCount++;
expect(member.typing).toBe(true);
});
client.on("RoomMember.powerLevel", function(event, member) {
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
powerLevelInvokeCount++;
});
client.on("RoomMember.membership", function(event, member) {
client!.on(RoomMemberEvent.Membership, function(event, member) {
membershipInvokeCount++;
expect(member.membership).toEqual("join");
});
client.startClient();
client!.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(typingInvokeCount).toEqual(
1, "RoomMember.typing fired wrong number of times",
);
expect(powerLevelInvokeCount).toEqual(
0, "RoomMember.powerLevel fired wrong number of times",
);
expect(nameInvokeCount).toEqual(
0, "RoomMember.name fired wrong number of times",
);
expect(membershipInvokeCount).toEqual(
1, "RoomMember.membership fired wrong number of times",
);
expect(typingInvokeCount).toEqual(1);
expect(powerLevelInvokeCount).toEqual(0);
expect(nameInvokeCount).toEqual(0);
expect(membershipInvokeCount).toEqual(1);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN' };
httpBackend.when("GET", "/sync").respond(401, error);
httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
client!.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
return httpBackend!.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(1);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
httpBackend.when("GET", "/sync").respond(401, error);
httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
client!.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
return httpBackend!.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(1);
});
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5,10 +5,11 @@ import { MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api";
import { IStore } from "../../src/store";
describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something";
let httpBackend = null;
let httpBackend = new HttpBackend();
const userId = "@alice:localhost";
const userB = "@bob:localhost";
const accessToken = "aseukfgwef";
@@ -67,7 +68,7 @@ describe("MatrixClient opts", function() {
let client;
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: undefined,
baseUrl: baseUrl,
userId: userId,
@@ -99,7 +100,7 @@ describe("MatrixClient opts", function() {
];
client.on("event", function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1, "Recv unexpected event type: " + event.getType(),
-1,
);
expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1,
@@ -118,7 +119,7 @@ describe("MatrixClient opts", function() {
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
0,
);
});
});
@@ -127,8 +128,8 @@ describe("MatrixClient opts", function() {
let client;
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
store: new MemoryStore(),
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: new MemoryStore() as IStore,
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -141,12 +142,12 @@ describe("MatrixClient opts", function() {
});
it("shouldn't retry sending events", function(done) {
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({
errcode: "M_SOMETHING",
error: "Ruh roh",
}));
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
expect(false).toBe(true);
}, function(err) {
expect(err.errcode).toEqual("M_SOMETHING");
done();
+127
View File
@@ -0,0 +1,127 @@
/*
Copyright 2022 Dominik Henneke
Copyright 2022 Nordeck IT + Consulting GmbH.
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 HttpBackend from "matrix-mock-request";
import { Direction, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { TestClient } from "../TestClient";
describe("MatrixClient relations", () => {
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const setupTests = (): [MatrixClient, HttpBackend] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ scheduler },
);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
return [client, httpBackend];
};
beforeEach(() => {
[client, httpBackend] = setupTests();
});
afterEach(() => {
httpBackend!.verifyNoOutstandingExpectation();
return httpBackend!.stop();
});
it("should read related events with the default options", async () => {
const response = client!.relations(roomId, '$event-0', null, null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
});
it("should read related events with relation type", async () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
});
it("should read related events with relation type and event type", async () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
});
it("should read related events with custom options", async () => {
const response = client!.relations(roomId, '$event-0', null, null, {
dir: Direction.Forward,
from: 'FROM',
limit: 10,
to: 'TO',
});
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
});
it('should use default direction in the fetchRelations endpoint', async () => {
const response = client!.fetchRelations(roomId, '$event-0', null, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" });
});
});
+29 -23
View File
@@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventStatus, RoomEvent, MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import HttpBackend from "matrix-mock-request";
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() {
let client: MatrixClient = null;
let httpBackend: TestClient["httpBackend"] = null;
let scheduler;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let room: Room;
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
let room: Room | undefined;
beforeEach(function() {
scheduler = new MatrixScheduler();
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
@@ -37,15 +37,21 @@ describe("MatrixClient retrying", function() {
undefined,
{ scheduler },
);
httpBackend = testClient.httpBackend;
client = testClient.client;
room = new Room(roomId, client, userId);
client.store.storeRoom(room);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
const room = new Room(roomId, client, userId);
client!.store.storeRoom(room);
return [client, httpBackend, room];
};
beforeEach(function() {
[client, httpBackend, room] = setupTests();
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
httpBackend!.verifyNoOutstandingExpectation();
return httpBackend!.stop();
});
xit("should retry according to MatrixScheduler.retryFn", function() {
@@ -66,7 +72,7 @@ describe("MatrixClient retrying", function() {
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued
const p1 = client.sendMessage(roomId, {
const p1 = client!.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m1",
}).then(function() {
@@ -79,13 +85,13 @@ describe("MatrixClient retrying", function() {
// XXX: it turns out that the promise returned by this message
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client.sendMessage(roomId, {
client!.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m2",
});
// both events should be in the timeline at this point
const tl = room.getLiveTimeline().getEvents();
const tl = room!.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
const ev1 = tl[0];
const ev2 = tl[1];
@@ -94,24 +100,24 @@ describe("MatrixClient retrying", function() {
expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
client!.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() {
client.cancelPendingEvent(ev1);
client!.cancelPendingEvent(ev1);
}).toThrow();
}).respond(400); // fail the first message
// wait for the localecho of ev1 to be updated
const p3 = new Promise<void>((resolve, reject) => {
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
room!.on(RoomEvent.LocalEchoUpdated, (ev0) => {
if (ev0 === ev1) {
resolve();
}
@@ -121,7 +127,7 @@ describe("MatrixClient retrying", function() {
expect(tl.length).toEqual(1);
// cancel the first message
client.cancelPendingEvent(ev1);
client!.cancelPendingEvent(ev1);
expect(ev1.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(0);
});
@@ -129,7 +135,7 @@ describe("MatrixClient retrying", function() {
return Promise.all([
p1,
p3,
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
});
@@ -1,16 +1,35 @@
/*
Copyright 2022 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 HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import { RoomEvent } from "../../src";
import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() {
let client = null;
let httpBackend = null;
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
});
@@ -55,8 +74,7 @@ describe("MatrixClient room timelines", function() {
},
};
function setNextSyncData(events) {
events = events || [];
function setNextSyncData(events: Partial<IEvent>[] = []) {
NEXT_SYNC_DATA = {
next_batch: "n",
presence: { events: [] },
@@ -77,19 +95,9 @@ describe("MatrixClient room timelines", function() {
throw new Error("setNextSyncData only works with one room id");
}
if (e.state_key) {
if (e.__prev_event === undefined) {
throw new Error(
"setNextSyncData needs the prev state set to '__prev_event' " +
"for " + e.type,
);
}
if (e.__prev_event !== null) {
// push the previous state for this event type
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
}
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
} else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
} else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
@@ -97,7 +105,7 @@ describe("MatrixClient room timelines", function() {
});
}
beforeEach(async function() {
const setupTestClient = (): [MatrixClient, HttpBackend] => {
// these tests should work with or without timelineSupport
const testClient = new TestClient(
userId,
@@ -106,112 +114,117 @@ describe("MatrixClient room timelines", function() {
undefined,
{ timelineSupport: true },
);
httpBackend = testClient.httpBackend;
client = testClient.client;
const httpBackend = testClient.httpBackend;
const client = testClient.client;
setNextSyncData();
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, function() {
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
client.startClient();
client!.startClient();
return [client!, httpBackend];
};
beforeEach(async function() {
[client!, httpBackend] = setupTestClient();
await httpBackend.flush("/versions");
await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter");
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
httpBackend!.verifyNoOutstandingExpectation();
client!.stopClient();
return httpBackend!.stop();
});
describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client.sendTextMessage(roomId, "I am a fish", "txn1");
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);
expect(member?.userId).toEqual(userId);
expect(member?.name).toEqual(userName);
httpBackend.flush("/sync", 1).then(function() {
httpBackend!.flush("/sync", 1).then(function() {
done();
});
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"BEFORE the event comes down the event stream", function(done) {
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
msg: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client.on("sync", function(state) {
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 room = client!.getRoom(roomId)!;
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
httpBackend!.flush("/sync", 1).then(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
});
httpBackend.flush("/txn1", 1);
httpBackend!.flush("/txn1", 1);
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream", function(done) {
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
msg: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client.on("sync", function(state) {
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 room = client!.getRoom(roomId)!;
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend!.flush("/sync", 1).then(function() {
expect(room.timeline.length).toEqual(2);
httpBackend.flush("/txn1", 1);
httpBackend!.flush("/txn1", 1);
promise.then(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId);
@@ -219,7 +232,7 @@ describe("MatrixClient room timelines", function() {
});
});
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
});
@@ -229,7 +242,7 @@ describe("MatrixClient room timelines", function() {
beforeEach(function() {
sbEvents = [];
httpBackend.when("GET", "/messages").respond(200, function() {
httpBackend!.when("GET", "/messages").respond(200, function() {
return {
chunk: sbEvents,
start: "pagin_start",
@@ -240,26 +253,26 @@ describe("MatrixClient room timelines", function() {
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() {
client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBe(null);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
@@ -275,7 +288,7 @@ describe("MatrixClient room timelines", function() {
// make an m.room.member event for alice's join
const joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice",
url: null,
url: undefined,
});
// make an m.room.member event with prev_content for alice's nick
@@ -286,7 +299,7 @@ describe("MatrixClient room timelines", function() {
});
oldMshipEvent.prev_content = {
displayname: "Old Alice",
avatar_url: null,
avatar_url: undefined,
membership: "join",
};
@@ -303,32 +316,32 @@ describe("MatrixClient room timelines", function() {
joinMshipEvent,
];
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
// sync response
expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() {
client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(5);
const joinMsg = room.timeline[0];
expect(joinMsg.sender.name).toEqual("Old Alice");
expect(joinMsg.sender?.name).toEqual("Old Alice");
const oldMsg = room.timeline[1];
expect(oldMsg.sender.name).toEqual("Old Alice");
expect(oldMsg.sender?.name).toEqual("Old Alice");
const newMsg = room.timeline[3];
expect(newMsg.sender.name).toEqual(userName);
expect(newMsg.sender?.name).toEqual(userName);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should add it them to the right place in the timeline", function(done) {
@@ -342,27 +355,27 @@ describe("MatrixClient room timelines", function() {
}),
];
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() {
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(() => {
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should use 'end' as the next pagination token", function(done) {
@@ -373,25 +386,25 @@ describe("MatrixClient room timelines", function() {
}),
];
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
expect(room.oldState.paginationToken).toBeTruthy();
client.scrollback(room, 1).then(function() {
client!.scrollback(room, 1).then(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend.flush("/messages", 1).then(function() {
httpBackend!.flush("/messages", 1).then(function() {
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
});
@@ -404,23 +417,23 @@ describe("MatrixClient room timelines", function() {
setNextSyncData(eventData);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
let index = 0;
client.on("Room.timeline", function(event, rm, toStart) {
client!.on(RoomEvent.Timeline, function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
expect(event.event).toEqual(eventData[index]);
index += 1;
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(index).toEqual(2);
expect(room.timeline.length).toEqual(3);
@@ -442,22 +455,21 @@ describe("MatrixClient room timelines", function() {
}),
utils.mkMessage({ user: userId, room: roomId }),
];
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
expect(preNameEvent.sender?.name).toEqual(userName);
expect(postNameEvent.sender?.name).toEqual("New Name");
});
});
});
@@ -468,22 +480,21 @@ describe("MatrixClient room timelines", function() {
name: "Room 2",
},
});
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
let nameEmitCount = 0;
client.on("Room.name", function(rm) {
client!.on(RoomEvent.Name, function(rm) {
nameEmitCount += 1;
});
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
@@ -493,12 +504,11 @@ describe("MatrixClient room timelines", function() {
name: "Room 3",
},
});
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]);
}).then(function() {
expect(nameEmitCount).toEqual(2);
@@ -518,26 +528,24 @@ describe("MatrixClient room timelines", function() {
user: userC, room: roomId, mship: "invite", skey: userD,
}),
];
eventData[0].__prev_event = null;
eventData[1].__prev_event = null;
setNextSyncData(eventData);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
expect(room.currentState.getMember(userC)!.name).toEqual("C");
expect(room.currentState.getMember(userC)!.membership).toEqual(
"join",
);
expect(room.currentState.getMember(userD).name).toEqual(userD);
expect(room.currentState.getMember(userD).membership).toEqual(
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
expect(room.currentState.getMember(userD)!.membership).toEqual(
"invite",
);
});
@@ -554,26 +562,26 @@ describe("MatrixClient room timelines", function() {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend.flush("/versions", 1),
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/versions", 1),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId).name).toEqual(userName);
expect(room.currentState.getMember(userId).membership).toEqual(
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
expect(room.currentState.getMember(userId)!.membership).toEqual(
"join",
);
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId).membership).toEqual(
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(
"join",
);
});
@@ -588,21 +596,21 @@ describe("MatrixClient room timelines", function() {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
let emitCount = 0;
client.on("Room.timelineReset", function(emitRoom) {
client!.on(RoomEvent.TimelineReset, function(emitRoom) {
expect(emitRoom).toEqual(room);
emitCount++;
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(emitCount).toEqual(1);
});
@@ -618,7 +626,7 @@ describe("MatrixClient room timelines", function() {
];
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
`${encodeURIComponent(initialSyncEventData[2].event_id)}`;
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
const contextResponse = {
start: "start_token",
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
@@ -636,19 +644,19 @@ describe("MatrixClient room timelines", function() {
// Create a room from the sync
await Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 1),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Get the room after the first sync so the room is created
room = client.getRoom(roomId);
room = client!.getRoom(roomId)!;
expect(room).toBeTruthy();
});
it('should clear and refresh messages in timeline', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from.
httpBackend.when("GET", contextUrl)
httpBackend!.when("GET", contextUrl)
.respond(200, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
@@ -659,7 +667,7 @@ describe("MatrixClient room timelines", function() {
// Refresh the timeline.
await Promise.all([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
// Make sure the message are visible
@@ -681,7 +689,7 @@ describe("MatrixClient room timelines", function() {
// middle of all of this refresh timeline logic. We want to make
// sure the sync pagination still works as expected after messing
// the refresh timline logic messes with the pagination tokens.
httpBackend.when("GET", contextUrl)
httpBackend!.when("GET", contextUrl)
.respond(200, () => {
// Now finally return and make the `/context` request respond
return contextResponse;
@@ -700,7 +708,7 @@ describe("MatrixClient room timelines", function() {
const racingSyncEventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => {
const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => {
let eventFired = false;
// Throw a more descriptive error if this part of the test times out.
const failTimeout = setTimeout(() => {
@@ -726,12 +734,12 @@ describe("MatrixClient room timelines", function() {
// Then make a `/sync` happen by sending a message and seeing that it
// shows up (simulate a /sync naturally racing with us).
setNextSyncData(racingSyncEventData);
httpBackend.when("GET", "/sync").respond(200, function() {
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client, 1),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!, 1),
]);
// Make sure the timeline has the racey sync data
const afterRaceySyncTimelineEvents = room
@@ -761,7 +769,7 @@ describe("MatrixClient room timelines", function() {
await Promise.all([
refreshLiveTimelinePromise,
// Then flush the remaining `/context` to left the refresh logic complete
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
// Make sure sync pagination still works by seeing a new message show up
@@ -770,12 +778,12 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData);
httpBackend.when("GET", "/sync").respond(200, function() {
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 1),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Make sure the timeline includes the the events from the `/sync`
@@ -794,22 +802,19 @@ describe("MatrixClient room timelines", function() {
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from.
httpBackend.when("GET", contextUrl)
.respond(500, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
return {
errcode: 'TEST_FAKE_ERROR',
error: 'We purposely intercepted this /context request to make it fail ' +
'in order to test whether the refresh timeline code is resilient',
};
});
httpBackend!.when("GET", contextUrl).check(() => {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
}).respond(500, new MatrixError({
errcode: 'TEST_FAKE_ERROR',
error: 'We purposely intercepted this /context request to make it fail ' +
'in order to test whether the refresh timeline code is resilient',
}));
// Refresh the timeline and expect it to fail
const settledFailedRefreshPromises = await Promise.allSettled([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
// We only expect `TEST_FAKE_ERROR` here. Anything else is
// unexpected and should fail the test.
@@ -825,7 +830,7 @@ describe("MatrixClient room timelines", function() {
// `/messages` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` to construct a new timeline from.
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
.respond(200, function() {
return {
chunk: [{
@@ -837,7 +842,7 @@ describe("MatrixClient room timelines", function() {
// `/context` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
// timeline from.
httpBackend.when("GET", contextUrl)
httpBackend!.when("GET", contextUrl)
.respond(200, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
@@ -848,7 +853,7 @@ describe("MatrixClient room timelines", function() {
// Refresh the timeline again but this time it should pass
await Promise.all([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
// Make sure sync pagination still works by seeing a new message show up
@@ -857,12 +862,12 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData);
httpBackend.when("GET", "/sync").respond(200, function() {
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 1),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Make sure the message are visible
File diff suppressed because it is too large Load Diff
+21 -16
View File
@@ -95,26 +95,31 @@ describe("megolm key backups", function() {
return;
}
const Olm = global.Olm;
let testOlmAccount: Account;
let testOlmAccount: Olm.Account;
let aliceTestClient: TestClient;
const setupTestClient = (): [Account, TestClient] => {
const aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
const testOlmAccount = new Olm.Account();
testOlmAccount!.create();
return [testOlmAccount, aliceTestClient];
};
beforeAll(function() {
return Olm.init();
});
beforeEach(async function() {
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
testOlmAccount = new Olm.Account();
testOlmAccount.create();
await aliceTestClient.client.initCrypto();
aliceTestClient.client.crypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
[testOlmAccount, aliceTestClient] = setupTestClient();
await aliceTestClient!.client.initCrypto();
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
});
afterEach(function() {
return aliceTestClient.stop();
return aliceTestClient!.stop();
});
it("Alice checks key backups when receiving a message she can't decrypt", function() {
@@ -130,22 +135,22 @@ describe("megolm key backups", function() {
},
};
return aliceTestClient.start().then(() => {
return aliceTestClient!.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then(() => {
const privkey = decodeRecoveryKey(RECOVERY_KEY);
return aliceTestClient.client.crypto.storeSessionBackupPrivateKey(privkey);
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
}).then(() => {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient.expectKeyBackupQuery(
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient!.expectKeyBackupQuery(
ROOM_ID,
SESSION_ID,
200,
CURVE25519_KEY_BACKUP_DATA,
);
return aliceTestClient.httpBackend.flushAllExpected();
return aliceTestClient!.httpBackend.flushAllExpected();
}).then(function(): Promise<MatrixEvent> {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
if (event.getContent()) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+812
View File
@@ -0,0 +1,812 @@
/*
Copyright 2022 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.
*/
// eslint-disable-next-line no-restricted-imports
import MockHttpBackend from "matrix-mock-request";
import { fail } from "assert";
import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Extension } from "../../src/sliding-sync";
import { TestClient } from "../TestClient";
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
import {
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
} from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend | undefined;
let sdk: SlidingSyncSdk | undefined;
let mockSlidingSync: SlidingSync | undefined;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getList = 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();
s.setList = jest.fn();
s.setListRanges = jest.fn();
s.start = jest.fn();
s.stop = jest.fn();
s.resend = jest.fn();
return s;
};
// shorthand way to make events without filling in all the fields
let eventIdCounter = 0;
const mkOwnEvent = (evType: string, content: object): IRoomEvent => {
eventIdCounter++;
return {
type: evType,
content: content,
sender: selfUserId,
origin_server_ts: Date.now(),
event_id: "$" + eventIdCounter,
};
};
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
eventIdCounter++;
return {
type: evType,
state_key: stateKey,
content: content,
sender: selfUserId,
origin_server_ts: Date.now(),
event_id: "$" + eventIdCounter,
};
};
const assertTimelineEvents = (got: MatrixEvent[], want: IRoomEvent[]): void => {
expect(got.length).toEqual(want.length);
got.forEach((m, i) => {
expect(m.getType()).toEqual(want[i].type);
expect(m.getSender()).toEqual(want[i].sender);
expect(m.getId()).toEqual(want[i].event_id);
expect(m.getContent()).toEqual(want[i].content);
expect(m.getTs()).toEqual(want[i].origin_server_ts);
if (want[i].unsigned) {
expect(m.getUnsigned()).toEqual(want[i].unsigned);
}
const maybeStateEvent = want[i] as IStateEvent;
if (maybeStateEvent.state_key) {
expect(m.getStateKey()).toEqual(maybeStateEvent.state_key);
}
});
};
// assign client/httpBackend globals
const setupClient = async (testOpts?: Partial<IStoredClientOpts&{withCrypto: boolean}>) => {
testOpts = testOpts || {};
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto();
testOpts.crypto = client!.crypto;
}
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
};
// tear down client/httpBackend globals
const teardownClient = () => {
client!.stopClient();
return httpBackend!.stop();
};
// find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension => {
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
// find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension;
if (calledExtension && calledExtension.name() === name) {
return calledExtension;
}
}
fail("cannot find extension " + name);
};
describe("sync/stop", () => {
beforeAll(async () => {
await setupClient();
});
afterAll(teardownClient);
it("can sync()", async () => {
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
expect(mockSlidingSync!.start).toBeCalled();
});
it("can stop()", async () => {
sdk!.stop();
expect(mockSlidingSync!.stop).toBeCalled();
});
});
describe("rooms", () => {
beforeAll(async () => {
await setupClient();
});
afterAll(teardownClient);
describe("initial", () => {
beforeAll(async () => {
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
});
// inject some rooms with different fields set.
// All rooms are new so they all have initial: true
const roomA = "!a_state_and_timeline:localhost";
const roomB = "!b_timeline_only:localhost";
const roomC = "!c_with_highlight_count:localhost";
const roomD = "!d_with_notif_count:localhost";
const roomE = "!e_with_invite:localhost";
const roomF = "!f_calc_room_name:localhost";
const roomG = "!g_join_invite_counts:localhost";
const data: Record<string, MSC3575RoomData> = {
[roomA]: {
name: "A",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
],
timeline: [
mkOwnEvent(EventType.RoomMessage, { body: "hello A" }),
mkOwnEvent(EventType.RoomMessage, { body: "world A" }),
],
initial: true,
},
[roomB]: {
name: "B",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
],
initial: true,
},
[roomC]: {
name: "C",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
mkOwnEvent(EventType.RoomMessage, { body: "world C" }),
],
highlight_count: 5,
initial: true,
},
[roomD]: {
name: "D",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
mkOwnEvent(EventType.RoomMessage, { body: "world D" }),
],
notification_count: 5,
initial: true,
},
[roomE]: {
name: "E",
required_state: [],
timeline: [],
invite_state: [
{
type: EventType.RoomMember,
content: { membership: "invite" },
state_key: selfUserId,
sender: "@bob:localhost",
event_id: "$room_e_invite",
origin_server_ts: 123456,
},
{
type: "m.room.join_rules",
content: { join_rule: "invite" },
state_key: "",
sender: "@bob:localhost",
event_id: "$room_e_join_rule",
origin_server_ts: 123456,
},
],
initial: true,
},
[roomF]: {
name: "#foo:localhost",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "This should be ignored" }, ""),
],
timeline: [
mkOwnEvent(EventType.RoomMessage, { body: "hello A" }),
mkOwnEvent(EventType.RoomMessage, { body: "world A" }),
],
initial: true,
},
[roomG]: {
name: "G",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
],
joined_count: 5,
invited_count: 2,
initial: true,
},
};
it("can be created with required_state and timeline", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomA].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
});
it("can be created with timeline only", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomB].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
});
it("can be created with a highlight_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(data[roomC].highlight_count);
});
it("can be created with a notification_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(data[roomD].notification_count);
});
it("can be created with an invited/joined_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
});
it("can be created with invite_state", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
});
it("uses the 'name' field to caluclate the room name", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
const gotRoom = client!.getRoom(roomF);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.name,
).toEqual(data[roomF].name);
});
describe("updating", () => {
it("can update with a new timeline event", async () => {
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
timeline: [newEvent],
required_state: [],
name: data[roomA].name,
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
});
it("can update with a new required_state event", async () => {
let gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
required_state: [
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
],
timeline: [],
name: data[roomB].name,
});
gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
});
it("can update with a new highlight_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, {
name: data[roomC].name,
required_state: [],
timeline: [],
highlight_count: 1,
});
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(1);
});
it("can update with a new notification_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, {
name: data[roomD].name,
required_state: [],
timeline: [],
notification_count: 1,
});
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(1);
});
it("can update with a new joined_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, {
name: data[roomD].name,
required_state: [],
timeline: [],
joined_count: 1,
});
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
});
// Regression test for a bug which caused the timeline entries to be out-of-order
// when the same room appears twice with different timeline limits. E.g appears in
// the list with timeline_limit:1 then appears again as a room subscription with
// timeline_limit:50
it("can return history with a larger timeline_limit", async () => {
const timeline = data[roomA].timeline;
const oldTimeline = [
mkOwnEvent(EventType.RoomMessage, { body: "old event A" }),
mkOwnEvent(EventType.RoomMessage, { body: "old event B" }),
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
...timeline,
];
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
timeline: oldTimeline,
required_state: [],
name: data[roomA].name,
initial: true, // e.g requested via room subscription
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
(e) => (e.getType() + " : " + e.getContent().body)),
);
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline);
});
});
});
});
describe("lifecycle", () => {
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
});
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
{ pos: "h", lists: [], rooms: {}, extensions: {} },
);
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
);
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
);
}
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
});
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle,
SlidingSyncState.Complete,
{ pos: "i", lists: [], rooms: {}, extensions: {} },
);
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
});
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
expect(mockSlidingSync!.stop).not.toBeCalled();
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
errcode: "M_UNKNOWN_TOKEN",
message: "Oh no your access token is no longer valid",
}));
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
expect(mockSlidingSync!.stop).toBeCalled();
});
});
describe("opts", () => {
afterEach(teardownClient);
it("can resolveProfilesToInvites", async () => {
await setupClient({
resolveInvitesToProfiles: true,
});
const roomId = "!resolveProfilesToInvites:localhost";
const invitee = "@invitee:localhost";
const inviteeProfile = {
avatar_url: "mxc://foobar",
displayname: "The Invitee",
};
httpBackend!.when("GET", "/profile").respond(200, inviteeProfile);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
initial: true,
name: "Room with Invite",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
],
});
await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name);
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
const inviteeMember = room.getMember(invitee)!;
expect(inviteeMember).toBeDefined();
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
});
});
describe("ExtensionE2EE", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient({
withCrypto: true,
});
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("e2ee");
});
afterAll(async () => {
// needed else we do some async operations in the background which can cause Jest to whine:
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
// Attempted to log "Saving device tracking data null"."
client!.crypto!.stop();
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("can update device lists", () => {
ext.onResponse({
device_lists: {
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
},
});
// TODO: more assertions?
});
it("can update OTK counts", () => {
client!.crypto!.updateOneTimeKeyCount = 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);
});
});
describe("ExtensionAccountData", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("account_data");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes global account data", async () => {
const globalType = "global_test";
const globalContent = {
info: "here",
};
let globalData = client!.getAccountData(globalType);
expect(globalData).toBeUndefined();
ext.onResponse({
global: [
{
type: globalType,
content: globalContent,
},
],
});
globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeDefined();
expect(globalData.getContent()).toEqual(globalContent);
});
it("processes rooms account data", async () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with account data",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
initial: true,
});
const roomContent = {
foo: "bar",
};
const roomType = "test";
ext.onResponse({
rooms: {
[roomId]: [
{
type: roomType,
content: roomContent,
},
],
},
});
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
const event = room.getAccountData(roomType)!;
expect(event).toBeDefined();
expect(event.getContent()).toEqual(roomContent);
});
it("doesn't crash for unknown room account data", async () => {
const unknownRoomId = "!unknown:id";
const roomType = "tester";
ext.onResponse({
rooms: {
[unknownRoomId]: [
{
type: roomType,
content: {
foo: "Bar",
},
},
],
},
});
const room = client!.getRoom(unknownRoomId);
expect(room).toBeNull();
expect(client!.getAccountData(roomType)).toBeUndefined();
});
it("can update push rules via account data", async () => {
const roomId = "!foo:bar";
const pushRulesContent: IPushRules = {
global: {
[PushRuleKind.RoomSpecific]: [{
enabled: true,
default: true,
pattern: "monkey",
actions: [
{
set_tweak: TweakName.Sound,
value: "default",
},
],
rule_id: roomId,
}],
},
};
let pushRule = client!.getRoomPushRule("global", roomId);
expect(pushRule).toBeUndefined();
ext.onResponse({
global: [
{
type: EventType.PushRules,
content: pushRulesContent,
},
],
});
pushRule = client!.getRoomPushRule("global", roomId)!;
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
});
});
describe("ExtensionToDevice", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("to_device");
});
it("gets enabled with a limit on the initial request only", () => {
const reqJson: any = ext.onRequest(true);
expect(reqJson.enabled).toEqual(true);
expect(reqJson.limit).toBeGreaterThan(0);
expect(reqJson.since).toBeUndefined();
});
it("updates the since value", async () => {
ext.onResponse({
next_batch: "12345",
events: [],
});
expect(ext.onRequest(false)).toEqual({
since: "12345",
});
});
it("can handle missing fields", async () => {
ext.onResponse({
next_batch: "23456",
// no events array
});
});
it("emits to-device events on the client", async () => {
const toDeviceType = "custom_test";
const toDeviceContent = {
foo: "bar",
};
let called = false;
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
expect(ev.getContent()).toEqual(toDeviceContent);
expect(ev.getType()).toEqual(toDeviceType);
called = true;
});
ext.onResponse({
next_batch: "34567",
events: [
{
type: toDeviceType,
content: toDeviceContent,
},
],
});
expect(called).toBe(true);
});
it("can cancel key verification requests", async () => {
const seen: Record<string, boolean> = {};
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
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);
}
});
ext.onResponse({
next_batch: "45678",
events: [
// someone tries to verify keys
{
type: "m.key.verification.start",
content: {
transaction_id: "a",
},
},
{
type: "m.key.verification.request",
content: {
transaction_id: "a",
},
},
// then gives up
{
type: "m.key.verification.cancel",
content: {
transaction_id: "a",
},
},
],
});
});
});
});
File diff suppressed because it is too large Load Diff
+1 -9
View File
@@ -16,20 +16,12 @@ limitations under the License.
*/
import { logger } from '../src/logger';
import * as utils from "../src/utils";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
global.Olm = require('@matrix-org/olm');
logger.log('loaded libolm');
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available");
}
// also try to set node crypto
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support: some tests will fail');
}
+25
View File
@@ -0,0 +1,25 @@
/*
Copyright 2022 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 DOMException from "domexception";
global.DOMException = DOMException;
jest.mock("../src/http-api/utils", () => ({
...jest.requireActual("../src/http-api/utils"),
// We mock timeoutSignal otherwise it causes tests to leave timers running
timeoutSignal: () => new AbortController().signal,
}));
+94
View File
@@ -0,0 +1,94 @@
/*
Copyright 2022 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 { MethodLikeKeys, mocked, MockedObject } from "jest-mock";
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { User } from "../../src/models/user";
/**
* Mock client with real event emitter
* useful for testing code that listens
* to MatrixClient events
*/
export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
constructor(mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> = {}) {
super();
Object.assign(this, mockProperties);
}
}
/**
* - make a mock client
* - cast the type to mocked(MatrixClient)
* - spy on MatrixClientPeg.get to return the mock
* eg
* ```
* const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId),
});
* ```
*/
export const getMockClientWithEventEmitter = (
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
): MockedObject<MatrixClient> => {
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
return mock;
};
/**
* Returns basic mocked client methods related to the current user
* ```
* const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser('@mytestuser:domain'),
});
* ```
*/
export const mockClientMethodsUser = (userId = '@alice:domain') => ({
getUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
});
/**
* Returns basic mocked client methods related to rendering events
* ```
* const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser('@mytestuser:domain'),
});
* ```
*/
export const mockClientMethodsEvents = () => ({
decryptEventIfNeeded: jest.fn(),
getPushActionsForEvent: jest.fn(),
});
/**
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
doesServerSupportSeparateAddAndBind: jest.fn(),
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
});
+1 -1
View File
@@ -24,5 +24,5 @@ limitations under the License.
* expect(beaconLivenessEmits.length).toBe(1);
* ```
*/
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
spy.mock.calls.filter((args) => args[0] === eventType);
+28
View File
@@ -0,0 +1,28 @@
/*
Copyright 2022 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.
*/
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
export function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}
+71 -20
View File
@@ -5,8 +5,8 @@ import EventEmitter from "events";
import '../olm-loader';
import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, MatrixClient, MsgType } from "../../src";
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
@@ -70,13 +70,15 @@ export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
interface IEventOpts {
type: EventType | string;
room: string;
room?: string;
sender?: string;
skey?: string;
content: IContent;
prev_content?: IContent;
user?: string;
unsigned?: IUnsigned;
redacts?: string;
ts?: number;
}
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
@@ -93,8 +95,8 @@ let testEventIndex = 1; // counter for events, easier for comparison of randomly
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): object;
export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixClient): object | MatrixEvent {
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixClient): Partial<IEvent> | MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
@@ -103,10 +105,12 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
prev_content: opts.prev_content,
unsigned: opts.unsigned || {},
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
redacts: opts.redacts,
origin_server_ts: opts.ts ?? 0,
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
@@ -129,12 +133,27 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
return opts.event ? new MatrixEvent(event) : event;
}
type GeneratedMetadata = {
event_id: string;
txn_id: string;
origin_server_ts: number;
};
export function mkEventCustom<T>(base: T): T & GeneratedMetadata {
return {
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
origin_server_ts: Date.now(),
...base,
};
}
interface IPresenceOpts {
user?: string;
sender?: string;
url: string;
name: string;
ago: number;
url?: string;
name?: string;
ago?: number;
presence?: string;
event?: boolean;
}
@@ -145,8 +164,8 @@ interface IPresenceOpts {
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent;
export function mkPresence(opts: IPresenceOpts & { event?: false }): object;
export function mkPresence(opts: IPresenceOpts & { event?: boolean }): object | MatrixEvent {
export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial<IEvent>;
export function mkPresence(opts: IPresenceOpts & { event?: boolean }): Partial<IEvent> | MatrixEvent {
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
@@ -162,7 +181,7 @@ export function mkPresence(opts: IPresenceOpts & { event?: boolean }): object |
}
interface IMembershipOpts {
room: string;
room?: string;
mship: string;
sender?: string;
user?: string;
@@ -186,8 +205,8 @@ interface IMembershipOpts {
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent;
export function mkMembership(opts: IMembershipOpts & { event?: false }): object;
export function mkMembership(opts: IMembershipOpts & { event?: boolean }): object | MatrixEvent {
export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial<IEvent>;
export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Partial<IEvent> | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMember,
@@ -208,11 +227,25 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): objec
return mkEvent(eventOpts);
}
interface IMessageOpts {
room: string;
export function mkMembershipCustom<T>(
base: T & { membership: string, sender: string, content?: IContent },
): T & { type: EventType, sender: string, state_key: string, content: IContent } & GeneratedMetadata {
const content = base.content || {};
return mkEventCustom({
...base,
content: { ...content, membership: base.membership },
type: EventType.RoomMember,
state_key: base.sender,
});
}
export interface IMessageOpts {
room?: string;
user: string;
msg?: string;
event?: boolean;
relatesTo?: IEventRelation;
ts?: number;
}
/**
@@ -226,8 +259,11 @@ interface IMessageOpts {
* @return {Object|MatrixEvent} The event
*/
export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): object;
export function mkMessage(opts: IMessageOpts & { event?: boolean }, client?: MatrixClient): object | MatrixEvent {
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
export function mkMessage(
opts: IMessageOpts & { event?: boolean },
client?: MatrixClient,
): Partial<IEvent> | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
@@ -237,6 +273,10 @@ export function mkMessage(opts: IMessageOpts & { event?: boolean }, client?: Mat
},
};
if (opts.relatesTo) {
eventOpts.content["m.relates_to"] = opts.relatesTo;
}
if (!eventOpts.content.body) {
eventOpts.content.body = "Random->" + Math.random();
}
@@ -260,11 +300,11 @@ interface IReplyMessageOpts extends IMessageOpts {
* @return {Object|MatrixEvent} The event
*/
export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): object;
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
export function mkReplyMessage(
opts: IReplyMessageOpts & { event?: boolean },
client?: MatrixClient,
): object | MatrixEvent {
): Partial<IEvent> | MatrixEvent {
const eventOpts: IEventOpts = {
...opts,
type: EventType.RoomMessage,
@@ -275,7 +315,7 @@ export function mkReplyMessage(
"rel_type": "m.in_reply_to",
"event_id": opts.replyToMessage.getId(),
"m.in_reply_to": {
"event_id": opts.replyToMessage.getId(),
"event_id": opts.replyToMessage.getId()!,
},
},
},
@@ -341,3 +381,14 @@ export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent>
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",
data: {},
device_display_name: "name",
kind: "http",
lang: "en",
pushkey: "pushpush",
...extra,
});
+134
View File
@@ -0,0 +1,134 @@
/*
Copyright 2022 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 { RelationType } from "../../src/@types/event";
import { MatrixClient } from "../../src/client";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Thread } from "../../src/models/thread";
import { mkMessage } from "./test-utils";
export const makeThreadEvent = ({ rootEventId, replyToEventId, ...props }: any & {
rootEventId: string; replyToEventId: string; event?: boolean;
}): MatrixEvent => mkMessage({
...props,
relatesTo: {
event_id: rootEventId,
rel_type: "m.thread",
['m.in_reply_to']: {
event_id: replyToEventId,
},
},
});
type MakeThreadEventsProps = {
roomId: Room["roomId"];
// root message user id
authorId: string;
// user ids of thread replies
// cycled through until thread length is fulfilled
participantUserIds: string[];
// number of messages in the thread, root message included
// optional, default 2
length?: number;
ts?: number;
// provide to set current_user_participated accurately
currentUserId?: string;
};
export const makeThreadEvents = ({
roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId,
}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => {
const rootEvent = mkMessage({
user: authorId,
room: roomId,
msg: 'root event message ' + Math.random(),
ts,
event: true,
});
const rootEventId = rootEvent.getId();
const events = [rootEvent];
for (let i = 1; i < length; i++) {
const prevEvent = events[i - 1];
const replyToEventId = prevEvent.getId();
const user = participantUserIds[i % participantUserIds.length];
events.push(makeThreadEvent({
user,
room: roomId,
event: true,
msg: `reply ${i} by ${user}`,
rootEventId,
replyToEventId,
// replies are 1ms after each other
ts: ts + i,
}));
}
rootEvent.setUnsigned({
"m.relations": {
[RelationType.Thread]: {
latest_event: events[events.length - 1],
count: length,
current_user_participated: [...participantUserIds, authorId].includes(currentUserId ?? ""),
},
},
});
return { rootEvent, events };
};
type MakeThreadProps = {
room: Room;
client: MatrixClient;
authorId: string;
participantUserIds: string[];
length?: number;
ts?: number;
};
export const mkThread = ({
room,
client,
authorId,
participantUserIds,
length = 2,
ts = 1,
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => {
const { rootEvent, events } = makeThreadEvents({
roomId: room.roomId,
authorId,
participantUserIds,
length,
ts,
currentUserId: client.getUserId() ?? "",
});
expect(rootEvent).toBeTruthy();
for (const evt of events) {
room?.reEmitter.reEmit(evt, [
MatrixEventEvent.BeforeRedaction,
]);
}
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
// So that we do not have to mock the thread loading
thread.initialEventsFetched = true;
thread.addEvents(events, true);
return { thread, rootEvent, events };
};
+503
View File
@@ -0,0 +1,503 @@
/*
Copyright 2022 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 {
ClientEvent,
ClientEventHandlerMap,
EventType,
GroupCall,
GroupCallIntent,
GroupCallType,
IContent,
ISendEventResponse,
MatrixClient,
MatrixEvent,
Room,
RoomState,
RoomStateEvent,
RoomStateEventHandlerMap,
} from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
import { SyncState } from "../../src/sync";
import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call";
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
import { CallFeed } from "../../src/webrtc/callFeed";
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler";
export const DUMMY_SDP = (
"v=0\r\n" +
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
"a=ice-options:trickle\r\n" +
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
"a=setup:active\r\n" +
"a=mid:0\r\n" +
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
"a=sendrecv\r\n" +
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
"a=rtcp-mux\r\n" +
"a=rtpmap:111 opus/48000/2\r\n" +
"a=rtcp-fb:111 transport-cc\r\n" +
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
"a=rtpmap:103 ISAC/16000\r\n" +
"a=rtpmap:104 ISAC/32000\r\n" +
"a=rtpmap:9 G722/8000\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:8 PCMA/8000\r\n" +
"a=rtpmap:106 CN/32000\r\n" +
"a=rtpmap:105 CN/16000\r\n" +
"a=rtpmap:13 CN/8000\r\n" +
"a=rtpmap:110 telephone-event/48000\r\n" +
"a=rtpmap:112 telephone-event/32000\r\n" +
"a=rtpmap:113 telephone-event/16000\r\n" +
"a=rtpmap:126 telephone-event/8000\r\n" +
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
);
export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler";
export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler";
class MockMediaStreamAudioSourceNode {
public connect() {}
}
class MockAnalyser {
public getFloatFrequencyData() { return 0.0; }
}
export class MockAudioContext {
constructor() {}
public createAnalyser() { return new MockAnalyser(); }
public createMediaStreamSource() { return new MockMediaStreamAudioSourceNode(); }
public close() {}
}
export class MockRTCPeerConnection {
private static instances: MockRTCPeerConnection[] = [];
private negotiationNeededListener?: () => void;
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
public onTrackListener?: (e: RTCTrackEvent) => void;
public needsNegotiation = false;
public readyToNegotiate: Promise<void>;
private onReadyToNegotiate?: () => void;
public localDescription: RTCSessionDescription;
public signalingState: RTCSignalingState = "stable";
public transceivers: MockRTCRtpTransceiver[] = [];
public static triggerAllNegotiations(): void {
for (const inst of this.instances) {
inst.doNegotiation();
}
}
public static hasAnyPendingNegotiations(): boolean {
return this.instances.some(i => i.needsNegotiation);
}
public static resetInstances() {
this.instances = [];
}
constructor() {
this.localDescription = {
sdp: DUMMY_SDP,
type: 'offer',
toJSON: function() { },
};
this.readyToNegotiate = new Promise<void>(resolve => {
this.onReadyToNegotiate = resolve;
});
MockRTCPeerConnection.instances.push(this);
}
public addEventListener(type: string, listener: () => void) {
if (type === 'negotiationneeded') {
this.negotiationNeededListener = listener;
} else if (type == 'icecandidate') {
this.iceCandidateListener = listener;
} else if (type == 'track') {
this.onTrackListener = listener;
}
}
public createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
public createOffer() {
return Promise.resolve({
type: 'offer',
sdp: DUMMY_SDP,
});
}
public createAnswer() {
return Promise.resolve({
type: 'answer',
sdp: DUMMY_SDP,
});
}
public setRemoteDescription() {
return Promise.resolve();
}
public setLocalDescription() {
return Promise.resolve();
}
public close() { }
public getStats() { return []; }
public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
this.needsNegotiation = true;
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
const newSender = new MockRTCRtpSender(track);
const newReceiver = new MockRTCRtpReceiver(track);
const newTransceiver = new MockRTCRtpTransceiver(this);
newTransceiver.sender = newSender as unknown as RTCRtpSender;
newTransceiver.receiver = newReceiver as unknown as RTCRtpReceiver;
this.transceivers.push(newTransceiver);
return newTransceiver;
}
public addTrack(track: MockMediaStreamTrack): MockRTCRtpSender {
return this.addTransceiver(track).sender as unknown as MockRTCRtpSender;
}
public removeTrack() {
this.needsNegotiation = true;
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
}
public getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; }
public getSenders(): MockRTCRtpSender[] {
return this.transceivers.map(t => t.sender as unknown as MockRTCRtpSender);
}
public doNegotiation() {
if (this.needsNegotiation && this.negotiationNeededListener) {
this.needsNegotiation = false;
this.negotiationNeededListener();
}
}
}
export class MockRTCRtpSender {
constructor(public track: MockMediaStreamTrack) { }
public replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
}
export class MockRTCRtpReceiver {
constructor(public track: MockMediaStreamTrack) { }
}
export class MockRTCRtpTransceiver {
constructor(private peerConn: MockRTCPeerConnection) {}
public sender?: RTCRtpSender;
public receiver?: RTCRtpReceiver;
public set direction(_: string) {
this.peerConn.needsNegotiation = true;
}
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
}
export class MockMediaStreamTrack {
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
public stop = jest.fn<void, []>();
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public settings?: MediaTrackSettings;
public getSettings(): MediaTrackSettings { return this.settings!; }
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
public typed(): MediaStreamTrack { return this as unknown as MediaStreamTrack; }
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
export class MockMediaStream {
constructor(
public id: string,
private tracks: MockMediaStreamTrack[] = [],
) {}
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public getTracks() { return this.tracks; }
public getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
public getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
public addTrack(track: MockMediaStreamTrack) {
this.tracks.push(track);
this.dispatchEvent("addtrack");
}
public removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
public clone(): MediaStream {
return new MockMediaStream(this.id + ".clone", this.tracks).typed();
}
public isCloneOf(stream: MediaStream) {
return this.id === stream.id + ".clone";
}
// syntactic sugar for typing
public typed(): MediaStream {
return this as unknown as MediaStream;
}
}
export class MockMediaDeviceInfo {
constructor(
public kind: "audioinput" | "videoinput" | "audiooutput",
) { }
public typed(): MediaDeviceInfo { return this as unknown as MediaDeviceInfo; }
}
export class MockMediaHandler {
public userMediaStreams: MockMediaStream[] = [];
public screensharingStreams: MockMediaStream[] = [];
public getUserMediaStream(audio: boolean, video: boolean) {
const tracks: MockMediaStreamTrack[] = [];
if (audio) tracks.push(new MockMediaStreamTrack("usermedia_audio_track", "audio"));
if (video) tracks.push(new MockMediaStreamTrack("usermedia_video_track", "video"));
const stream = new MockMediaStream(USERMEDIA_STREAM_ID, tracks);
this.userMediaStreams.push(stream);
return stream;
}
public stopUserMediaStream(stream: MockMediaStream) {
stream.isStopped = true;
}
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
const stream = new MockMediaStream(SCREENSHARE_STREAM_ID, tracks);
this.screensharingStreams.push(stream);
return stream;
});
public stopScreensharingStream(stream: MockMediaStream) {
stream.isStopped = true;
}
public hasAudioDevice() { return true; }
public hasVideoDevice() { return true; }
public stopAllStreams() {}
public typed(): MediaHandler { return this as unknown as MediaHandler; }
}
export class MockMediaDevices {
public enumerateDevices = jest.fn<Promise<MediaDeviceInfo[]>, []>().mockResolvedValue([
new MockMediaDeviceInfo("audioinput").typed(),
new MockMediaDeviceInfo("videoinput").typed(),
]);
public getUserMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
Promise.resolve(new MockMediaStream("local_stream").typed()),
);
public getDisplayMedia = jest.fn<Promise<MediaStream>, [DisplayMediaStreamConstraints]>().mockReturnValue(
Promise.resolve(new MockMediaStream("local_display_stream").typed()),
);
public typed(): MediaDevices { return this as unknown as MediaDevices; }
}
type EmittedEvents = CallEventHandlerEvent | CallEvent | ClientEvent | RoomStateEvent | GroupCallEventHandlerEvent;
type EmittedEventMap = CallEventHandlerEventHandlerMap &
CallEventHandlerMap &
ClientEventHandlerMap &
RoomStateEventHandlerMap &
GroupCallEventHandlerMap;
export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, EmittedEventMap> {
public mediaHandler = new MockMediaHandler();
constructor(public userId: string, public deviceId: string, public sessionId: string) {
super();
}
public groupCallEventHandler = {
groupCalls: new Map<string, GroupCall>(),
};
public callEventHandler = {
calls: new Map<string, MatrixCall>(),
};
public sendStateEvent = jest.fn<Promise<ISendEventResponse>, [
roomId: string, eventType: EventType, content: any, statekey: string,
]>();
public sendToDevice = jest.fn<Promise<{}>, [
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
txnId?: string,
]>();
public getMediaHandler(): MediaHandler { return this.mediaHandler.typed(); }
public getUserId(): string { return this.userId; }
public getDeviceId(): string { return this.deviceId; }
public getSessionId(): string { return this.sessionId; }
public getTurnServers = () => [];
public isFallbackICEServerAllowed = () => false;
public reEmitter = new ReEmitter(new TypedEventEmitter());
public getUseE2eForGroupCall = () => false;
public checkTurnServers = () => null;
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();
public typed(): MatrixClient { return this as unknown as MatrixClient; }
public emitRoomState(event: MatrixEvent, state: RoomState): void {
this.emit(
RoomStateEvent.Events,
event,
state,
null,
);
}
}
export class MockCallFeed {
constructor(
public userId: string,
public stream: MockMediaStream,
) {}
public measureVolumeActivity(val: boolean) {}
public dispose() {}
public typed(): CallFeed {
return this as unknown as CallFeed;
}
}
export function installWebRTCMocks() {
global.navigator = {
mediaDevices: new MockMediaDevices().typed(),
} as unknown as Navigator;
global.window = {
// @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock
RTCSessionDescription: {},
// @ts-ignore Mock
RTCIceCandidate: {},
getUserMedia: () => new MockMediaStream("local_stream"),
};
// @ts-ignore Mock
global.document = {};
// @ts-ignore Mock
global.AudioContext = MockAudioContext;
// @ts-ignore Mock
global.RTCRtpReceiver = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
};
// @ts-ignore Mock
global.RTCRtpSender = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
};
}
export function makeMockGroupCallStateEvent(roomId: string, groupCallId: string, content: IContent = {
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
}): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue(content),
getStateKey: jest.fn().mockReturnValue(groupCallId),
} as unknown as MatrixEvent;
}
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue({}),
getStateKey: jest.fn().mockReturnValue(groupCallId),
} as unknown as MatrixEvent;
}
+13 -8
View File
@@ -21,34 +21,37 @@ describe("NamespacedValue", () => {
const ns = new NamespacedValue("stable", "unstable");
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBe(ns.unstable);
expect(ns.names).toEqual([ns.stable, ns.unstable]);
});
it("should return unstable if there is no stable", () => {
const ns = new NamespacedValue(null, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
});
it("should have a falsey unstable if needed", () => {
const ns = new NamespacedValue("stable", null);
const ns = new NamespacedValue("stable");
expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.stable]);
});
it("should match against either stable or unstable", () => {
const ns = new NamespacedValue("stable", "unstable");
expect(ns.matches("no")).toBe(false);
expect(ns.matches(ns.stable)).toBe(true);
expect(ns.matches(ns.unstable)).toBe(true);
expect(ns.matches(ns.stable!)).toBe(true);
expect(ns.matches(ns.unstable!)).toBe(true);
});
it("should not permit falsey values for both parts", () => {
try {
new UnstableValue(null, null);
new UnstableValue(null!, null!);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.message).toBe("One of stable or unstable values must be supplied");
expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
}
});
});
@@ -58,21 +61,23 @@ describe("UnstableValue", () => {
const ns = new UnstableValue("stable", "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBe(ns.stable);
expect(ns.names).toEqual([ns.unstable, ns.stable]);
});
it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null, "unstable");
const ns = new UnstableValue(null!, "unstable");
expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
});
it("should not permit falsey unstable values", () => {
try {
new UnstableValue("stable", null);
new UnstableValue("stable", null!);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.message).toBe("Unstable value must be supplied");
expect((<Error>e).message).toBe("Unstable value must be supplied");
}
});
});
@@ -1,6 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 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.
@@ -17,19 +17,19 @@ limitations under the License.
import MockHttpBackend from "matrix-mock-request";
import * as sdk from "../../src";
import { AutoDiscovery } from "../../src/autodiscovery";
describe("AutoDiscovery", function() {
let httpBackend = null;
beforeEach(function() {
httpBackend = new MockHttpBackend();
sdk.request(httpBackend.requestFn);
});
const getHttpBackend = (): MockHttpBackend => {
const httpBackend = new MockHttpBackend();
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
return httpBackend;
};
it("should throw an error when no domain is specified", function() {
getHttpBackend();
return Promise.all([
// @ts-ignore testing no args
AutoDiscovery.findClientConfig(/* no args */).then(() => {
throw new Error("Expected a failure, not success with no args");
}, () => {
@@ -42,13 +42,13 @@ describe("AutoDiscovery", function() {
return true;
}),
AutoDiscovery.findClientConfig(null).then(() => {
AutoDiscovery.findClientConfig(null as any).then(() => {
throw new Error("Expected a failure, not success with null");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(true).then(() => {
AutoDiscovery.findClientConfig(true as any).then(() => {
throw new Error("Expected a failure, not success with a non-string");
}, () => {
return true;
@@ -57,6 +57,7 @@ describe("AutoDiscovery", function() {
});
it("should return PROMPT when .well-known 404s", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
return Promise.all([
httpBackend.flushAllExpected(),
@@ -80,6 +81,7 @@ describe("AutoDiscovery", function() {
});
it("should return FAIL_PROMPT when .well-known returns a 500 error", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
return Promise.all([
httpBackend.flushAllExpected(),
@@ -103,6 +105,7 @@ describe("AutoDiscovery", function() {
});
it("should return FAIL_PROMPT when .well-known returns a 400 error", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
return Promise.all([
httpBackend.flushAllExpected(),
@@ -126,6 +129,7 @@ describe("AutoDiscovery", function() {
});
it("should return FAIL_PROMPT when .well-known returns an empty body", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
return Promise.all([
httpBackend.flushAllExpected(),
@@ -148,31 +152,31 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
it("should return FAIL_PROMPT when .well-known returns not-JSON", async () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc", true);
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
AutoDiscovery.findClientConfig("example.org").then(
expect(expected).toEqual,
),
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (empty string)", function() {
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (empty string)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "",
@@ -199,8 +203,8 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (no property)", function() {
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (no property)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {},
});
@@ -225,8 +229,8 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (disallowed scheme)", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for m.homeserver (disallowed scheme)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "mxc://example.org",
@@ -255,6 +259,7 @@ describe("AutoDiscovery", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 404)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -284,6 +289,7 @@ describe("AutoDiscovery", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 500)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -313,6 +319,7 @@ describe("AutoDiscovery", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 200 but wrong content)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
not_matrix_versions: ["r0.0.1"],
});
@@ -344,8 +351,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " +
"m.homeserver", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri).toEqual("https://example.org/_matrix/client/versions");
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
@@ -376,8 +384,9 @@ describe("AutoDiscovery", function() {
});
it("should return SUCCESS with the right homeserver URL", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -411,8 +420,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (missing base_url)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -451,8 +461,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (empty base_url)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -491,8 +502,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (validation error: 404)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -532,8 +544,9 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (validation error: 500)", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
@@ -573,14 +586,15 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS when the identity server configuration is " +
"verifiably accurate", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
@@ -615,14 +629,15 @@ describe("AutoDiscovery", function() {
it("should return SUCCESS and preserve non-standard keys from the " +
".well-known response", function() {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
expect(req.path)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
@@ -660,4 +675,76 @@ describe("AutoDiscovery", function() {
}),
]);
});
it("should return FAIL_PROMPT for connection errors", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined!);
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT for fetch errors", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, new Error("CORS or something"));
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT for invalid JSON", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "<html>", true);
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
});
+61
View File
@@ -22,6 +22,7 @@ import {
makeBeaconContent,
makeBeaconInfoContent,
makeTopicContent,
parseBeaconContent,
parseTopicContent,
} from "../../src/content-helpers";
@@ -127,6 +128,66 @@ describe('Beacon content helpers', () => {
});
});
});
describe("parseBeaconContent()", () => {
it("should not explode when parsing an invalid beacon", () => {
// deliberate cast to simulate wire content being invalid
const result = parseBeaconContent({} as any);
expect(result).toEqual({
description: undefined,
uri: undefined,
timestamp: undefined,
});
});
it("should parse unstable values", () => {
const uri = "urigoeshere";
const description = "descriptiongoeshere";
const timestamp = 1234;
const result = parseBeaconContent({
"org.matrix.msc3488.location": {
uri,
description,
},
"org.matrix.msc3488.ts": timestamp,
// relationship not used - just here to satisfy types
"m.relates_to": {
rel_type: "m.reference",
event_id: "$unused",
},
});
expect(result).toEqual({
description,
uri,
timestamp,
});
});
it("should parse stable values", () => {
const uri = "urigoeshere";
const description = "descriptiongoeshere";
const timestamp = 1234;
const result = parseBeaconContent({
"m.location": {
uri,
description,
},
"m.ts": timestamp,
// relationship not used - just here to satisfy types
"m.relates_to": {
rel_type: "m.reference",
event_id: "$unused",
},
});
expect(result).toEqual({
description,
uri,
timestamp,
});
});
});
});
describe('Topic content helpers', () => {
-59
View File
@@ -1,59 +0,0 @@
import { getHttpUriForMxc } from "../../src/content-repo";
describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
describe("getHttpUriForMxc", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
const httpUrl = "http://example.com/image.jpeg";
expect(
getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true,
),
).toEqual(httpUrl);
});
it("should return the empty string HTTP URLs by default", function() {
const httpUrl = "http://example.com/image.jpeg";
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
});
it("should return a download URL if no width/height/resize are specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
);
});
it("should return the empty string for null input", function() {
expect(getHttpUriForMxc(null)).toEqual("");
});
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop",
);
});
it("should put fragments from mxc:// URIs after any query parameters",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32#automade",
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
);
});
});
});
+75
View File
@@ -0,0 +1,75 @@
/*
Copyright 2022 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 { getHttpUriForMxc } from "../../src/content-repo";
describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
describe("getHttpUriForMxc", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
const httpUrl = "http://example.com/image.jpeg";
expect(
getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true,
),
).toEqual(httpUrl);
});
it("should return the empty string HTTP URLs by default", function() {
const httpUrl = "http://example.com/image.jpeg";
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
});
it("should return a download URL if no width/height/resize are specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
);
});
it("should return the empty string for null input", function() {
expect(getHttpUriForMxc(null as any, '')).toEqual("");
});
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop",
);
});
it("should put fragments from mxc:// URIs after any query parameters",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32#automade",
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
);
});
});
});
-470
View File
@@ -1,470 +0,0 @@
import '../olm-loader';
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { Crypto } from "../../src/crypto";
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../MockStorageApi";
import { TestClient } from "../TestClient";
import { MatrixEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import * as olmlib from "../../src/crypto/olmlib";
import { sleep } from "../../src/utils";
import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from '../../src/logger';
import { MemoryStore } from "../../src";
const Olm = global.Olm;
function awaitEvent(emitter, event) {
return new Promise((resolve, reject) => {
emitter.once(event, (result) => {
resolve(result);
});
});
}
async function keyshareEventForEvent(client, event, index) {
const roomId = event.getRoomId();
const eventContent = event.getWireContent();
const key = await client.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
eventContent.sender_key,
eventContent.session_id,
index,
);
const ksEvent = new MatrixEvent({
type: "m.forwarded_room_key",
sender: client.getUserId(),
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: eventContent.sender_key,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
session_id: eventContent.session_id,
session_key: key.key,
chain_index: key.chain_index,
forwarding_curve25519_key_chain:
key.forwarding_curve_key_chain,
},
});
// make onRoomKeyEvent think this was an encrypted event
ksEvent.senderCurve25519Key = "akey";
return ksEvent;
}
describe("Crypto", function() {
if (!CRYPTO_ENABLED) {
return;
}
beforeAll(function() {
return Olm.init();
});
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(3);
});
describe("encrypted events", function() {
it("provides encryption information", async function() {
const client = (new TestClient(
"@alice:example.com", "deviceid",
)).client;
await client.initCrypto();
// unencrypted event
const event = {
getId: () => "$event_id",
getSenderKey: () => null,
getWireContent: () => {return {};},
};
let encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeFalsy();
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
event.getForwardingCurve25519KeyChain = () => ["not empty"];
event.isKeySourceUntrusted = () => false;
event.getClaimedEd25519Key =
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
expect(encryptionInfo.authenticated).toBeFalsy();
expect(encryptionInfo.sender).toBeFalsy();
// known sender, megolm key from backup
event.getForwardingCurve25519KeyChain = () => [];
event.isKeySourceUntrusted = () => true;
const device = new DeviceInfo("FLIBBLE");
device.keys["curve25519:FLIBBLE"] =
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client.crypto.deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
expect(encryptionInfo.authenticated).toBeFalsy();
expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeFalsy();
// known sender, trusted megolm key, but bad ed25519key
event.isKeySourceUntrusted = () => false;
device.keys["ed25519:FLIBBLE"] =
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
expect(encryptionInfo.authenticated).toBeTruthy();
expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeTruthy();
client.stopClient();
});
});
describe('Session management', function() {
const otkResponse = {
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:FLIBBLE': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally a valid signature',
},
},
},
},
},
},
};
let crypto;
let mockBaseApis;
let mockRoomList;
let fakeEmitter;
beforeEach(async function() {
const mockStorage = new MockStorageApi();
const clientStore = new MemoryStore({ localStorage: mockStorage });
const cryptoStore = new MemoryCryptoStore(mockStorage);
cryptoStore.storeEndToEndDeviceData({
devices: {
'@bob:home.server': {
'BOBDEVICE': {
keys: {
'curve25519:BOBDEVICE': 'this is a key',
},
},
},
},
trackingStatus: {},
});
mockBaseApis = {
sendToDevice: jest.fn(),
getKeyBackupVersion: jest.fn(),
isGuest: jest.fn(),
};
mockRoomList = {};
fakeEmitter = new EventEmitter();
crypto = new Crypto(
mockBaseApis,
"@alice:home.server",
"FLIBBLE",
clientStore,
cryptoStore,
mockRoomList,
);
crypto.registerEventHandlers(fakeEmitter);
await crypto.init();
});
afterEach(async function() {
await crypto.stop();
});
it("restarts wedged Olm sessions", async function() {
const prom = new Promise((resolve) => {
mockBaseApis.claimOneTimeKeys = function() {
resolve();
return otkResponse;
};
});
fakeEmitter.emit('toDeviceEvent', {
getId: jest.fn().mockReturnValue("$wedged"),
getType: jest.fn().mockReturnValue('m.room.message'),
getContent: jest.fn().mockReturnValue({
msgtype: 'm.bad.encrypted',
}),
getWireContent: jest.fn().mockReturnValue({
algorithm: 'm.olm.v1.curve25519-aes-sha2',
sender_key: 'this is a key',
}),
getSender: jest.fn().mockReturnValue('@bob:home.server'),
});
await prom;
});
});
describe('Key requests', function() {
let aliceClient;
let bobClient;
beforeEach(async function() {
aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await aliceClient.initCrypto();
await bobClient.initCrypto();
});
afterEach(async function() {
aliceClient.stopClient();
bobClient.stopClient();
});
it("does not cancel keyshare requests if some messages are not decrypted", 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", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
event.clearEvent = undefined;
event.senderCurve25519Key = null;
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
let eventPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
// keyshare the session key starting at the second message, so
// the first message can't be decrypted yet, but the second one
// can
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
const cryptoStore = bobClient.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,
};
// the room key request should still be there, since we haven't
// decrypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
// keyshare the session key starting at the first message, so
// that it can now be decrypted
eventPromise = awaitEvent(events[0], "Event.decrypted");
ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
await sleep(1);
// the room key request should be gone since we've now decrypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
});
it("should error if a forwarded room key lacks a content.sender_key", 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", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
const event = new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
});
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
event.clearEvent = undefined;
event.senderCurve25519Key = null;
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const ksEvent = await keyshareEventForEvent(aliceClient, event, 1);
ksEvent.getContent().sender_key = undefined; // test
bobClient.crypto.addInboundGroupSession = jest.fn();
await bobDecryptor.onRoomKeyEvent(ksEvent);
expect(bobClient.crypto.addInboundGroupSession).not.toHaveBeenCalled();
});
it("creates a new keyshare request if we request a keyshare", async function() {
// make sure that cancelAndResend... creates a new keyshare request
// if there wasn't an already-existing one
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
const cryptoStore = aliceClient.cryptoStore;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: "!someroom",
session_id: "sessionid",
sender_key: "senderkey",
};
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toBeDefined();
});
it("uses a new txnid for re-requesting keys", async function() {
jest.useFakeTimers();
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
// replace Alice's sendToDevice function with a mock
aliceClient.sendToDevice = jest.fn().mockResolvedValue(undefined);
aliceClient.startClient();
// make a room key request, and record the transaction ID for the
// sendToDevice call
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
// key requests get queued until the sync has finished, but we don't
// let the client set up enough for that to happen, so gut-wrench a bit
// to force it to send now.
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
jest.runAllTimers();
await Promise.resolve();
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
const txnId = aliceClient.sendToDevice.mock.calls[0][2];
// give the room key request manager time to update the state
// of the request
await Promise.resolve();
// cancel and resend the room key request
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
jest.runAllTimers();
await Promise.resolve();
// 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(aliceClient.sendToDevice).toBeCalledTimes(3);
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
});
});
describe('Secret storage', function() {
it("creates secret storage even if there is no keyInfo", async function() {
jest.spyOn(logger, 'log').mockImplementation(() => {});
jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto();
client.crypto.getSecretStorageKey = async () => null;
client.crypto.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = () => null;
client.crypto.baseApis.setAccountData = () => null;
client.crypto.baseApis.uploadKeySignatures = () => null;
client.crypto.baseApis.http.authedRequest = () => null;
const createSecretStorageKey = async () => {
return {
keyInfo: undefined, // Returning undefined here used to cause a crash
privateKey: Uint8Array.of(32, 33),
};
};
await client.crypto.bootstrapSecretStorage({
createSecretStorageKey,
});
client.stopClient();
});
});
});
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -222,9 +222,9 @@ describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
() => new IndexedDBCryptoStore(undefined!, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
const store = new IndexedDBCryptoStore(undefined!, "tests");
// @ts-ignore set private properties
store._backend = new MemoryCryptoStore();
// @ts-ignore
@@ -247,14 +247,14 @@ describe.each([
const olmDevice = new OlmDevice(store);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
createCryptoStoreCacheCallbacks(store, olmDevice);
await storeCrossSigningKeyCache("self_signing", testKey);
await storeCrossSigningKeyCache!("self_signing", testKey);
// If we've not saved anything, don't expect anything
// Definitely don't accidentally return the wrong key for the type
const nokey = await getCrossSigningKeyCache("self", "");
const nokey = await getCrossSigningKeyCache!("self", "");
expect(nokey).toBeNull();
const key = await getCrossSigningKeyCache("self_signing", "");
expect(new Uint8Array(key)).toEqual(testKey);
const key = await getCrossSigningKeyCache!("self_signing", "");
expect(new Uint8Array(key!)).toEqual(testKey);
});
});
+1 -1
View File
@@ -90,7 +90,7 @@ const signedDeviceList2: IDownloadKeyResult = {
describe('DeviceList', function() {
let downloadSpy;
let cryptoStore;
let deviceLists = [];
let deviceLists: DeviceList[] = [];
beforeEach(function() {
deviceLists = [];
@@ -1,18 +1,39 @@
/*
Copyright 2022 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, MockedObject } from 'jest-mock';
import '../../../olm-loader';
import * as algorithms from "../../../../src/crypto/algorithms";
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../../../MockStorageApi";
import * as testUtils from "../../../test-utils/test-utils";
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
import { Crypto } from "../../../../src/crypto";
import { Crypto, IncomingRoomKeyRequest } from "../../../../src/crypto";
import { logger } from "../../../../src/logger";
import { MatrixEvent } from "../../../../src/models/event";
import { TestClient } from "../../../TestClient";
import { Room } from "../../../../src/models/room";
import * as olmlib from "../../../../src/crypto/olmlib";
import { TypedEventEmitter } from '../../../../src/models/typed-event-emitter';
import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const ROOM_ID = '!ROOM:ID';
@@ -28,17 +49,20 @@ describe("MegolmDecryption", function() {
return Olm.init();
});
let megolmDecryption;
let mockOlmLib;
let mockCrypto;
let mockBaseApis;
let megolmDecryption: algorithms.DecryptionAlgorithm;
let mockOlmLib: MockedObject<typeof olmlib>;
let mockCrypto: MockedObject<Crypto>;
let mockBaseApis: MockedObject<MatrixClient>;
beforeEach(async function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockBaseApis = {};
mockCrypto = testUtils.mock(Crypto, 'Crypto') as MockedObject<Crypto>;
mockBaseApis = {
claimOneTimeKeys: jest.fn(),
sendToDevice: jest.fn(),
queueToDevice: jest.fn(),
} as unknown as MockedObject<MatrixClient>;
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
const cryptoStore = new MemoryCryptoStore();
const olmDevice = new OlmDevice(cryptoStore);
@@ -51,11 +75,15 @@ describe("MegolmDecryption", function() {
});
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
mockOlmLib.encryptMessageForDevice =
jest.fn().mockResolvedValue(undefined);
mockOlmLib = {
encryptMessageForDevice: jest.fn().mockResolvedValue(undefined),
ensureOlmSessionsForDevices: jest.fn(),
} as unknown as MockedObject<typeof olmlib>;
// @ts-ignore illegal assignment that makes these tests work :/
megolmDecryption.olmlib = mockOlmLib;
jest.clearAllMocks();
});
describe('receives some keys:', function() {
@@ -82,12 +110,18 @@ describe("MegolmDecryption", function() {
senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519",
};
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
const mockCrypto = {
decryptEvent: function() {
return Promise.resolve(decryptedData);
},
};
} as unknown as Crypto;
await event.attemptDecryption(mockCrypto).then(() => {
megolmDecryption.onRoomKeyEvent(event);
@@ -115,10 +149,13 @@ describe("MegolmDecryption", function() {
});
it('can respond to a key request event', function() {
const keyRequest = {
const keyRequest: IncomingRoomKeyRequest = {
requestId: '123',
share: jest.fn(),
userId: '@alice:foo',
deviceId: 'alidevice',
requestBody: {
algorithm: '',
room_id: ROOM_ID,
sender_key: "SENDER_CURVE25519",
session_id: groupSession.session_id(),
@@ -131,23 +168,25 @@ describe("MegolmDecryption", function() {
expect(hasKeys).toBe(true);
// set up some pre-conditions for the share call
const deviceInfo = {};
const deviceInfo = {} as DeviceInfo;
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
'@alice:foo': { 'alidevice': {
sessionId: 'alisession',
device: new DeviceInfo('alidevice'),
} },
});
const awaitEncryptForDevice = new Promise((res, rej) => {
const awaitEncryptForDevice = new Promise<void>((res, rej) => {
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
res();
return Promise.resolve();
});
});
mockBaseApis.sendToDevice = jest.fn();
mockBaseApis.sendToDevice.mockReset();
mockBaseApis.queueToDevice.mockReset();
// do the share
megolmDecryption.shareKeysWithDevice(keyRequest);
@@ -265,17 +304,18 @@ describe("MegolmDecryption", function() {
let olmDevice;
beforeEach(async () => {
// @ts-ignore assigning to readonly prop
mockCrypto.backupManager = {
backupGroupSession: () => {},
};
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
const cryptoStore = new MemoryCryptoStore();
olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = jest.fn();
await olmDevice.init();
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
mockBaseApis.claimOneTimeKeys.mockResolvedValue({
failures: {},
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
@@ -290,8 +330,9 @@ describe("MegolmDecryption", function() {
},
},
},
}));
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
});
mockBaseApis.sendToDevice.mockResolvedValue({});
mockBaseApis.queueToDevice.mockResolvedValue(undefined);
aliceDeviceInfo = {
deviceId: 'aliceDevice',
@@ -311,18 +352,30 @@ describe("MegolmDecryption", function() {
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
} as DeviceTrustLevel);
megolmEncryption = new MegolmEncryption({
userId: '@user:id',
deviceId: '12345',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
algorithm: 'm.megolm.v1.aes-sha2',
rotation_period_ms: rotationPeriodMs,
},
});
// Splice the real method onto the mock object as megolm uses this method
// on the crypto class in order to encrypt / start sessions
// @ts-ignore Mock
mockCrypto.encryptAndSendToDevices = Crypto.prototype.encryptAndSendToDevices;
// @ts-ignore Mock
mockCrypto.olmDevice = olmDevice;
// @ts-ignore Mock
mockCrypto.baseApis = mockBaseApis;
mockRoom = {
getEncryptionTargetMembers: jest.fn().mockReturnValue(
[{ userId: "@alice:home.server" }],
@@ -369,7 +422,7 @@ describe("MegolmDecryption", function() {
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.queueToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
@@ -412,7 +465,7 @@ describe("MegolmDecryption", function() {
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWI',
);
mockBaseApis.sendToDevice.mockClear();
mockBaseApis.queueToDevice.mockClear();
await megolmEncryption.reshareKeyWithDevice(
olmDevice.deviceCurve25519Key,
ct1.session_id,
@@ -420,7 +473,7 @@ describe("MegolmDecryption", function() {
aliceDeviceInfo,
);
expect(mockBaseApis.sendToDevice).not.toHaveBeenCalled();
expect(mockBaseApis.queueToDevice).not.toHaveBeenCalled();
});
});
});
@@ -440,78 +493,54 @@ describe("MegolmDecryption", function() {
bobClient1.initCrypto(),
bobClient2.initCrypto(),
]);
const aliceDevice = aliceClient.crypto.olmDevice;
const bobDevice1 = bobClient1.crypto.olmDevice;
const bobDevice2 = bobClient2.crypto.olmDevice;
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice1 = bobClient1.crypto!.olmDevice;
const bobDevice2 = bobClient2.crypto!.olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobMember = new RoomMember(roomId, "@bob:example.com");
room.getEncryptionTargetMembers = async function() {
return [{ userId: "@bob:example.com" }];
return [bobMember];
};
room.setBlacklistUnverifiedDevices(true);
aliceClient.store.storeRoom(room);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
const BOB_DEVICES = {
const BOB_DEVICES: Record<string, IDevice> = {
bobdevice1: {
user_id: "@bob:example.com",
device_id: "bobdevice1",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
"ed25519:Dynabook": bobDevice1.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key!,
},
verified: 0,
known: false,
},
bobdevice2: {
user_id: "@bob:example.com",
device_id: "bobdevice2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
"ed25519:Dynabook": bobDevice2.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key!,
},
verified: -1,
known: false,
},
};
aliceClient.crypto.deviceList.storeDevicesForUser(
aliceClient.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
// @ts-ignore short-circuiting private method
return this.getDevicesFromStore(userIds);
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice2.session_id;
expect(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: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.blacklisted',
reason: 'The sender has blocked you.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
};
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
const event = new MatrixEvent({
type: "m.room.message",
@@ -523,15 +552,132 @@ describe("MegolmDecryption", function() {
body: "secret",
},
});
await aliceClient.crypto.encryptEvent(event, room);
await aliceClient.crypto!.encryptEvent(event, room);
expect(run).toBe(true);
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"].bobdevice2.session_id;
expect(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: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.blacklisted',
reason: 'The sender has blocked you.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
aliceClient.stopClient();
bobClient1.stopClient();
bobClient2.stopClient();
});
it("does not block unverified devices when sending verification events", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient.crypto!.olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobMember = new RoomMember(roomId, "@bob:example.com");
room.getEncryptionTargetMembers = async function() {
return [bobMember];
};
room.setBlacklistUnverifiedDevices(true);
aliceClient.store.storeRoom(room);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
const BOB_DEVICES: Record<string, IDevice> = {
bobdevice: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:bobdevice": bobDevice.deviceEd25519Key!,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
},
verified: 0,
known: true,
},
};
aliceClient.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
// @ts-ignore private
return this.getDevicesFromStore(userIds);
};
await bobDevice.generateOneTimeKeys(1);
const oneTimeKeys = await bobDevice.getOneTimeKeys();
const signedOneTimeKeys: Record<string, { key: string, signatures: object }> = {};
for (const keyId in oneTimeKeys.curve25519) {
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
const k = {
key: oneTimeKeys.curve25519[keyId],
signatures: {},
};
signedOneTimeKeys["signed_curve25519:" + keyId] = k;
await bobClient.crypto!.signObject(k);
break;
}
}
aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({
one_time_keys: {
'@bob:example.com': {
bobdevice: signedOneTimeKeys,
},
},
failures: {},
});
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
const event = new MatrixEvent({
type: "m.key.verification.start",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$event",
content: {
from_device: "alicedevice",
method: "m.sas.v1",
transaction_id: "transactionid",
},
});
await aliceClient.crypto!.encryptEvent(event, room);
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toEqual("m.room.encrypted");
aliceClient.stopClient();
bobClient.stopClient();
});
it("notifies devices when unable to create olm session", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
@@ -543,8 +689,8 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const aliceDevice = aliceClient.crypto.olmDevice;
const bobDevice = bobClient.crypto.olmDevice;
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice = bobClient.crypto!.olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
@@ -557,63 +703,46 @@ describe("MegolmDecryption", function() {
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
aliceRoom.getEncryptionTargetMembers = async () => {
return [
{
userId: "@alice:example.com",
membership: "join",
},
{
userId: "@bob:example.com",
membership: "join",
},
];
};
aliceRoom.getEncryptionTargetMembers = jest.fn().mockResolvedValue([
{
userId: "@alice:example.com",
membership: "join",
},
{
userId: "@bob:example.com",
membership: "join",
},
]);
const BOB_DEVICES = {
bobdevice: {
user_id: "@bob:example.com",
device_id: "bobdevice",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:bobdevice": bobDevice.deviceEd25519Key,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key,
"ed25519:bobdevice": bobDevice.deviceEd25519Key!,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key!,
},
known: true,
verified: 1,
},
};
aliceClient.crypto.deviceList.storeDevicesForUser(
aliceClient.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
aliceClient.crypto!.deviceList.downloadKeys = async function(userIds) {
// @ts-ignore private
return this.getDevicesFromStore(userIds);
};
aliceClient.claimOneTimeKeys = async () => {
aliceClient.claimOneTimeKeys = jest.fn().mockResolvedValue({
// Bob has no one-time keys
return {
one_time_keys: {},
};
};
const sendPromise = new Promise((resolve, reject) => {
aliceClient.sendToDevice = async (msgtype, contentMap) => {
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
resolve();
};
one_time_keys: {},
failures: {},
});
aliceClient.sendToDevice = jest.fn().mockResolvedValue({});
const event = new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
@@ -621,8 +750,22 @@ describe("MegolmDecryption", function() {
event_id: "$event",
content: {},
});
await aliceClient.crypto.encryptEvent(event, aliceRoom);
await sendPromise;
await aliceClient.crypto!.encryptEvent(event, aliceRoom);
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
aliceClient.stopClient();
bobClient.stopClient();
});
@@ -638,12 +781,15 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient.crypto.olmDevice;
const bobDevice = bobClient.crypto!.olmDevice;
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
const roomId = "!someroom";
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
@@ -655,7 +801,7 @@ describe("MegolmDecryption", function() {
},
}));
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -669,7 +815,7 @@ describe("MegolmDecryption", function() {
},
}))).rejects.toThrow("The sender has blocked you.");
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
@@ -682,7 +828,7 @@ describe("MegolmDecryption", function() {
},
}));
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -710,15 +856,19 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
aliceClient.crypto.downloadKeys = async () => {};
const bobDevice = bobClient.crypto.olmDevice;
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
aliceClient.crypto!.downloadKeys = jest.fn();
const bobDevice = bobClient.crypto!.olmDevice;
const roomId = "!someroom";
const now = Date.now();
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
@@ -734,7 +884,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100);
});
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -749,7 +899,7 @@ describe("MegolmDecryption", function() {
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
@@ -766,7 +916,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100);
});
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -795,15 +945,18 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient.crypto.olmDevice;
aliceClient.crypto.downloadKeys = async () => {};
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
const bobDevice = bobClient.crypto!.olmDevice;
aliceClient.crypto!.downloadKeys = jest.fn();
const roomId = "!someroom";
const now = Date.now();
// pretend we got an event that we can't decrypt
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
aliceEventEmitter.emit(ClientEvent.ToDeviceEvent, new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
content: {
@@ -818,7 +971,7 @@ describe("MegolmDecryption", function() {
setTimeout(resolve, 100);
});
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
await expect(aliceClient.crypto!.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
@@ -1,6 +1,6 @@
/*
Copyright 2018,2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,17 +15,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from 'jest-mock';
import '../../../olm-loader';
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import { MockStorageApi } from "../../../MockStorageApi";
import { logger } from "../../../../src/logger";
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
import * as olmlib from "../../../../src/crypto/olmlib";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { MatrixClient } from '../../../../src';
function makeOlmDevice() {
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
const cryptoStore = new MemoryCryptoStore();
const olmDevice = new OlmDevice(cryptoStore);
return olmDevice;
}
@@ -51,8 +52,8 @@ describe("OlmDevice", function() {
return global.Olm.init();
});
let aliceOlmDevice;
let bobOlmDevice;
let aliceOlmDevice: OlmDevice;
let bobOlmDevice: OlmDevice;
beforeEach(async function() {
aliceOlmDevice = makeOlmDevice();
@@ -66,13 +67,13 @@ describe("OlmDevice", function() {
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
bobOlmDevice.deviceCurve25519Key!,
sid,
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
) as any; // OlmDevice.encryptMessage has incorrect return type
const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
aliceOlmDevice.deviceCurve25519Key!,
ciphertext.type,
ciphertext.body,
);
@@ -93,16 +94,16 @@ describe("OlmDevice", function() {
+ " in the family Proteidae"
);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
bobOlmDevice.deviceCurve25519Key!,
sessionId,
MESSAGE,
);
) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedOlmDevice = makeOlmDevice();
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
const decrypted = await bobRecreatedOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
aliceOlmDevice.deviceCurve25519Key!,
ciphertext.type,
ciphertext.body,
);
@@ -117,17 +118,17 @@ describe("OlmDevice", function() {
+ " the olm is entirely aquatic"
);
const ciphertext2 = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
bobOlmDevice.deviceCurve25519Key!,
sessionId,
MESSAGE_2,
);
) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedAgainOlmDevice = makeOlmDevice();
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
// Note: "decrypted_2" does not have the same structure as "decrypted"
const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage(
aliceOlmDevice.deviceCurve25519Key,
aliceOlmDevice.deviceCurve25519Key!,
decrypted.session_id,
ciphertext2.type,
ciphertext2.body,
@@ -148,7 +149,7 @@ describe("OlmDevice", function() {
setTimeout(reject, 500);
});
},
};
} as unknown as MockedObject<MatrixClient>;
const devicesByUser = {
"@bob:example.com": [
DeviceInfo.fromStorage({
@@ -205,7 +206,7 @@ describe("OlmDevice", function() {
setTimeout(reject, 500);
});
},
};
} as unknown as MockedObject<MatrixClient>;
const deviceBobA = DeviceInfo.fromStorage({
keys: {
+69 -37
View File
@@ -30,11 +30,11 @@ import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
import { StubStore } from "../../../src/store/stub";
import { IAbortablePromise, MatrixScheduler } from '../../../src';
import { MatrixScheduler } from '../../../src';
const Olm = global.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const ROOM_ID = '!ROOM:ID';
@@ -131,7 +131,7 @@ function makeTestClient(cryptoStore) {
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: jest.fn(), // NOP
fetchFn: jest.fn(), // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
@@ -197,7 +197,7 @@ describe("MegolmBackup", function() {
// to tick the clock between the first try and the retry.
const realSetTimeout = global.setTimeout;
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
return realSetTimeout(f, n/100);
return realSetTimeout(f!, n!/100);
});
});
@@ -214,6 +214,12 @@ describe("MegolmBackup", function() {
const event = new MatrixEvent({
type: 'm.room.encrypted',
});
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
const decryptedData = {
clearEvent: {
type: 'm.room_key',
@@ -292,27 +298,27 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
@@ -343,7 +349,7 @@ describe("MegolmBackup", function() {
return client.initCrypto()
.then(() => {
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
})
.then(() => {
return cryptoStore.doTxn(
@@ -375,27 +381,27 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
@@ -433,7 +439,7 @@ describe("MegolmBackup", function() {
new Promise<void>((resolve, reject) => {
let backupInfo;
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
@@ -443,23 +449,23 @@ describe("MegolmBackup", function() {
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
data.auth_data, client.getCrossSigningId(), "@alice:bar",
(data as Record<string, any>).auth_data, client.getCrossSigningId()!, "@alice:bar",
);
} catch (e) {
reject(e);
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({});
}
backupInfo = data;
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({});
} else if (numCalls === 2) {
expect(method).toBe("GET");
expect(path).toBe("/room_keys/version");
resolve();
return Promise.resolve(backupInfo) as IAbortablePromise<any>;
return Promise.resolve(backupInfo);
} else {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many times"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({});
}
};
}),
@@ -489,7 +495,7 @@ describe("MegolmBackup", function() {
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: jest.fn(), // NOP
fetchFn: jest.fn(), // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
@@ -536,33 +542,33 @@ describe("MegolmBackup", function() {
let numCalls = 0;
await new Promise<void>((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
} else {
return Promise.reject(
new Error("this is an expected failure"),
) as IAbortablePromise<any>;
);
}
};
return client.crypto.backupManager.backupGroupSession(
return client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
@@ -693,4 +699,30 @@ describe("MegolmBackup", function() {
)).rejects.toThrow();
});
});
describe("flagAllGroupSessionsForBackup", () => {
it("should return number of sesions needing backup", async () => {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
fetchFn: jest.fn(), // NOP
store,
scheduler,
userId: "@alice:bar",
deviceId: "device",
cryptoStore,
});
await client.initCrypto();
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6);
client.stopClient();
});
});
});
+91 -37
View File
@@ -93,8 +93,8 @@ describe("Cross Signing", function() {
);
alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => {
await olmlib.verifySignature(
alice.crypto.olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
alice.crypto!.olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!,
);
});
alice.uploadKeySignatures = async () => ({ failures: {} });
@@ -141,7 +141,7 @@ describe("Cross Signing", function() {
};
alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({});
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T> => ({} as T);
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
const authUploadDeviceSigningKeys = async func => await func({});
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
@@ -152,7 +152,7 @@ describe("Cross Signing", function() {
authUploadDeviceSigningKeys,
});
} catch (e) {
if (e.errcode === "M_FORBIDDEN") {
if ((<MatrixError>e).errcode === "M_FORBIDDEN") {
bootstrapDidThrow = true;
}
}
@@ -169,7 +169,7 @@ describe("Cross Signing", function() {
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -238,12 +238,12 @@ describe("Cross Signing", function() {
alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => {
try {
await olmlib.verifySignature(
alice.crypto.olmDevice,
alice.crypto!.olmDevice,
content["@alice:example.com"][
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
],
"@alice:example.com",
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
"Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!,
);
olmlib.pkVerify(
content["@alice:example.com"]["Osborne2"],
@@ -258,7 +258,7 @@ describe("Cross Signing", function() {
});
// @ts-ignore private property
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
@@ -266,7 +266,7 @@ describe("Cross Signing", function() {
keys: deviceInfo.keys,
algorithms: deviceInfo.algorithms,
};
await alice.crypto.signObject(aliceDevice);
await alice.crypto!.signObject(aliceDevice);
olmlib.pkSign(
aliceDevice as ISignedKey,
selfSigningKey as unknown as PkSigning,
@@ -401,7 +401,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -435,7 +435,7 @@ describe("Cross Signing", function() {
verified: 0,
known: false,
};
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Bob's device key should be TOFU
@@ -467,11 +467,11 @@ describe("Cross Signing", function() {
const aliceKeys: Record<string, PkSigning> = {};
const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
null,
undefined,
aliceKeys,
);
alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com");
alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {};
alice.crypto!.deviceList.startTrackingDeviceList("@bob:example.com");
alice.crypto!.deviceList.stopTrackingAllDeviceLists = () => {};
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
@@ -486,7 +486,7 @@ describe("Cross Signing", function() {
]);
const keyChangePromise = new Promise<void>((resolve, reject) => {
alice.crypto.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
alice.crypto!.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => {
if (userId === "@bob:example.com") {
resolve();
}
@@ -494,7 +494,7 @@ describe("Cross Signing", function() {
});
// @ts-ignore private property
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
@@ -502,7 +502,7 @@ describe("Cross Signing", function() {
keys: deviceInfo.keys,
algorithms: deviceInfo.algorithms,
};
await alice.crypto.signObject(aliceDevice);
await alice.crypto!.signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account();
bobOlmAccount.create();
@@ -667,7 +667,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -690,7 +690,7 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey",
},
};
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice as unknown as IDevice,
});
// Bob's device key should be untrusted
@@ -735,7 +735,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -770,7 +770,7 @@ describe("Cross Signing", function() {
},
},
};
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Alice verifies Bob's SSK
@@ -802,7 +802,7 @@ describe("Cross Signing", function() {
["ed25519:" + bobMasterPubkey2]: sskSig2,
},
};
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
@@ -838,8 +838,8 @@ describe("Cross Signing", function() {
// Alice gets new signature for device
const sig2 = bobSigning2.sign(bobDeviceString);
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
bobDevice.signatures!["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
@@ -876,20 +876,20 @@ describe("Cross Signing", function() {
bob.uploadKeySignatures = async () => ({ failures: {} });
// set Bob's cross-signing key
await resetCrossSigningKeys(bob);
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: {
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key,
"ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key,
"curve25519:Dynabook": bob.crypto!.olmDevice.deviceCurve25519Key!,
"ed25519:Dynabook": bob.crypto!.olmDevice.deviceEd25519Key!,
},
verified: 1,
known: true,
},
});
alice.crypto.deviceList.storeCrossSigningForUser(
alice.crypto!.deviceList.storeCrossSigningForUser(
"@bob:example.com",
bob.crypto.crossSigningInfo.toStorage(),
bob.crypto!.crossSigningInfo.toStorage(),
);
alice.uploadDeviceSigningKeys = async () => ({});
@@ -909,8 +909,8 @@ describe("Cross Signing", function() {
expect(bobTrust.isTofu()).toBeTruthy();
// "forget" that Bob is trusted
delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"]
.keys.master.signatures["@alice:example.com"];
delete alice.crypto!.deviceList.crossSigningInfo["@bob:example.com"]
.keys.master.signatures!["@alice:example.com"];
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
@@ -919,9 +919,9 @@ describe("Cross Signing", function() {
upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve;
});
alice.crypto.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com");
await new Promise((resolve) => {
alice.crypto.on(CryptoEvent.UserTrustStatusChanged, resolve);
alice.crypto!.on(CryptoEvent.UserTrustStatusChanged, resolve);
});
await upgradePromise;
@@ -963,7 +963,7 @@ describe("Cross Signing", function() {
};
// Alice's device downloads the keys, but doesn't trust them yet
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
@@ -999,7 +999,7 @@ describe("Cross Signing", function() {
["ed25519:" + alicePubkey]: sig,
},
} };
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
[aliceDeviceId]: aliceCrossSignedDevice,
});
@@ -1042,7 +1042,7 @@ describe("Cross Signing", function() {
};
// Alice's device downloads the keys
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
@@ -1067,11 +1067,65 @@ describe("Cross Signing", function() {
"ed25519:Dynabook": "someOtherPubkey",
},
};
alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
[deviceId]: aliceNotCrossSignedDevice,
});
expect(alice.checkIfOwnDeviceCrossSigned(deviceId)).toBeFalsy();
alice.stopClient();
});
it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
["ed25519:" + alicePubkey]: alicePubkey,
},
};
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
aliceSSK.signatures = {
"@alice:example.com": {
["ed25519:" + aliceMasterPubkey]: sskSig,
},
};
// Alice's device downloads the keys
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
},
},
self_signing: aliceSSK,
},
firstUse: true,
crossSigningVerifiedBefore: false,
});
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
alice.stopClient();
});
it("checkIfOwnDeviceCrossSigned should sanely handle unknown users", async () => {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy();
alice.stopClient();
});
});
+1 -1
View File
@@ -39,7 +39,7 @@ export async function createSecretStorageKey(): Promise<IRecoveryKey> {
decryption.free();
return {
// `pubkey` not used anymore with symmetric 4S
keyInfo: { pubkey: storagePublicKey, key: undefined },
keyInfo: { pubkey: storagePublicKey, key: undefined! },
privateKey: storagePrivateKey,
};
}
+143
View File
@@ -0,0 +1,143 @@
/*
Copyright 2022 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 '../../olm-loader';
import { TestClient } from '../../TestClient';
import { logger } from '../../../src/logger';
import { DEHYDRATION_ALGORITHM } from '../../../src/crypto/dehydration';
const Olm = global.Olm;
describe("Dehydration", () => {
if (!global.Olm) {
logger.warn('Not running dehydration unit tests: libolm not present');
return;
}
beforeAll(function() {
return global.Olm.init();
});
it("should rehydrate a dehydrated device", async () => {
const key = new Uint8Array([1, 2, 3]);
const alice = new TestClient(
"@alice:example.com", "Osborne2", undefined, undefined,
{
cryptoCallbacks: {
getDehydrationKey: async t => key,
},
},
);
const dehydratedDevice = new Olm.Account();
dehydratedDevice.create();
alice.httpBackend.when("GET", "/dehydrated_device").respond(200, {
device_id: "ABCDEFG",
device_data: {
algorithm: DEHYDRATION_ALGORITHM,
account: dehydratedDevice.pickle(new Uint8Array(key)),
},
});
alice.httpBackend.when("POST", "/dehydrated_device/claim").respond(200, {
success: true,
});
expect((await Promise.all([
alice.client.rehydrateDevice(),
alice.httpBackend.flushAllExpected(),
]))[0])
.toEqual("ABCDEFG");
expect(alice.client.getDeviceId()).toEqual("ABCDEFG");
});
it("should dehydrate a device", async () => {
const key = new Uint8Array([1, 2, 3]);
const alice = new TestClient(
"@alice:example.com", "Osborne2", undefined, undefined,
{
cryptoCallbacks: {
getDehydrationKey: async t => key,
},
},
);
await alice.client.initCrypto();
alice.httpBackend.when("GET", "/room_keys/version").respond(404, {
errcode: "M_NOT_FOUND",
});
let pickledAccount = "";
alice.httpBackend.when("PUT", "/dehydrated_device")
.check((req) => {
expect(req.data.device_data).toMatchObject({
algorithm: DEHYDRATION_ALGORITHM,
account: expect.any(String),
});
pickledAccount = req.data.device_data.account;
})
.respond(200, {
device_id: "ABCDEFG",
});
alice.httpBackend.when("POST", "/keys/upload/ABCDEFG")
.check((req) => {
expect(req.data).toMatchObject({
"device_keys": expect.objectContaining({
algorithms: expect.any(Array),
device_id: "ABCDEFG",
user_id: "@alice:example.com",
keys: expect.objectContaining({
"ed25519:ABCDEFG": expect.any(String),
"curve25519:ABCDEFG": expect.any(String),
}),
signatures: expect.objectContaining({
"@alice:example.com": expect.objectContaining({
"ed25519:ABCDEFG": expect.any(String),
}),
}),
}),
"one_time_keys": expect.any(Object),
"org.matrix.msc2732.fallback_keys": expect.any(Object),
});
})
.respond(200, {});
try {
const deviceId =
(await Promise.all([
alice.client.createDehydratedDevice(new Uint8Array(key), {}),
alice.httpBackend.flushAllExpected(),
]))[0];
expect(deviceId).toEqual("ABCDEFG");
expect(deviceId).not.toEqual("");
// try to rehydrate the dehydrated device
const rehydrated = new Olm.Account();
try {
rehydrated.unpickle(new Uint8Array(key), pickledAccount);
} finally {
rehydrated.free();
}
} finally {
alice.client?.crypto?.dehydrationManager?.stop();
alice.client?.crypto?.deviceList.stop();
}
});
});
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import { CryptoStore } from '../../../src/crypto/store/base';
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
@@ -26,36 +26,39 @@ import 'jest-localstorage-mock';
const requests = [
{
requestId: "A",
requestBody: { session_id: "A", room_id: "A" },
requestBody: { session_id: "A", room_id: "A", sender_key: "A", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
},
{
requestId: "B",
requestBody: { session_id: "B", room_id: "B" },
requestBody: { session_id: "B", room_id: "B", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@carrie:example.com", deviceId: "barbazquux" },
],
},
{
requestId: "C",
requestBody: { session_id: "C", room_id: "C" },
requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Unsent,
recipients: [
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
},
];
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
// @ts-ignore set private properties
store.backend = new MemoryCryptoStore();
// @ts-ignore
store.backendPromise = Promise.resolve(store.backend);
return store;
}],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("Outgoing room key requests [%s]", function(name, dbFactory) {
let store;
let store: CryptoStore;
beforeAll(async () => {
store = dbFactory();
@@ -75,13 +78,22 @@ describe.each([
});
});
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target",
async () => {
const r = await store.getOutgoingRoomKeyRequestsByTarget(
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent],
);
expect(r).toHaveLength(1);
expect(r[0]).toEqual(requests[0]);
});
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
async () => {
const r =
await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]);
expect(r).not.toBeNull();
expect(r).not.toBeUndefined();
expect(r.state).toEqual(RoomKeyRequestState.Sent);
expect(r!.state).toEqual(RoomKeyRequestState.Sent);
expect(requests).toContainEqual(r);
});
});
+43 -52
View File
@@ -21,19 +21,11 @@ import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from '../../TestClient';
import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes";
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from '../../../src/logger';
import * as utils from "../../../src/utils";
import { ICreateClientOpts } from '../../../src/client';
import { ClientEvent, ICreateClientOpts } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support');
}
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
const client = (new TestClient(
@@ -49,7 +41,7 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt
await client.initCrypto();
// No need to download keys for these tests
jest.spyOn(client.crypto, 'downloadKeys').mockResolvedValue({});
jest.spyOn(client.crypto!, 'downloadKeys').mockResolvedValue({});
return client;
}
@@ -101,30 +93,27 @@ describe("Secrets", function() {
},
},
);
alice.crypto.crossSigningInfo.setKeys({
alice.crypto!.crossSigningInfo.setKeys({
master: signingkeyInfo,
});
const secretStorage = alice.crypto.secretStorage;
const secretStorage = alice.crypto!.secretStorage;
jest.spyOn(alice, 'setAccountData').mockImplementation(
async function(eventType, contents, callback) {
async function(eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
if (callback) {
callback(undefined, undefined);
}
return {};
});
const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
};
await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master');
await alice.crypto!.crossSigningInfo.signObject(keyAccountData, 'master');
alice.store.storeAccountDataEvents([
new MatrixEvent({
@@ -191,7 +180,7 @@ describe("Secrets", function() {
},
},
);
alice.setAccountData = async function(eventType, contents, callback) {
alice.setAccountData = async function(eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
@@ -211,7 +200,7 @@ describe("Secrets", function() {
await alice.storeSecret("foo", "bar");
const accountData = alice.getAccountData('foo');
expect(accountData.getContent().encrypted).toBeTruthy();
expect(accountData!.getContent().encrypted).toBeTruthy();
alice.stopClient();
});
@@ -244,29 +233,29 @@ describe("Secrets", function() {
},
);
const vaxDevice = vax.client.crypto.olmDevice;
const osborne2Device = osborne2.client.crypto.olmDevice;
const secretStorage = osborne2.client.crypto.secretStorage;
const vaxDevice = vax.client.crypto!.olmDevice;
const osborne2Device = osborne2.client.crypto!.olmDevice;
const secretStorage = osborne2.client.crypto!.secretStorage;
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": {
user_id: "@alice:example.com",
device_id: "VAX",
known: false,
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:VAX": vaxDevice.deviceEd25519Key,
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
"ed25519:VAX": vaxDevice.deviceEd25519Key!,
"curve25519:VAX": vaxDevice.deviceCurve25519Key!,
},
verified: DeviceInfo.DeviceVerification.VERIFIED,
},
});
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": {
user_id: "@alice:example.com",
device_id: "Osborne2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
verified: 0,
known: false,
keys: {
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
"ed25519:Osborne2": osborne2Device.deviceEd25519Key!,
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key!,
},
},
});
@@ -275,15 +264,17 @@ describe("Secrets", function() {
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
await osborne2Device.markKeysAsPublished();
await vax.client.crypto.olmDevice.createOutboundSession(
osborne2Device.deviceCurve25519Key,
await vax.client.crypto!.olmDevice.createOutboundSession(
osborne2Device.deviceCurve25519Key!,
Object.values(otks)[0],
);
const request = await secretStorage.request("foo", ["VAX"]);
const secret = await request.promise;
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const request = await secretStorage.request("foo", ["VAX"]);
await request.promise; // return value not used
expect(secret).toBe("bar");
osborne2.stop();
vax.stop();
clearTestClientTimeouts();
@@ -329,7 +320,7 @@ describe("Secrets", function() {
);
bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
bob.setAccountData = async function(eventType, contents, callback) {
bob.setAccountData = async function(eventType, contents) {
const event = new MatrixEvent({
type: eventType,
content: contents,
@@ -337,7 +328,7 @@ describe("Secrets", function() {
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
this.emit(ClientEvent.AccountData, event);
return {};
};
@@ -348,8 +339,8 @@ describe("Secrets", function() {
createSecretStorageKey,
});
const crossSigning = bob.crypto.crossSigningInfo;
const secretStorage = bob.crypto.secretStorage;
const crossSigning = bob.crypto!.crossSigningInfo;
const secretStorage = bob.crypto!.secretStorage;
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
@@ -446,6 +437,7 @@ describe("Secrets", function() {
return [keyId, secretStorageKeys[keyId]];
}
}
return null;
},
},
},
@@ -495,7 +487,7 @@ describe("Secrets", function() {
},
}),
]);
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
firstUse: false,
crossSigningVerifiedBefore: false,
keys: {
@@ -537,16 +529,15 @@ describe("Secrets", function() {
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
this.emit(ClientEvent.AccountData, event);
return {};
};
await alice.bootstrapSecretStorage({});
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent())
.toEqual({ key: "key_id" });
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
.getContent() as ISecretStorageKeyInfo;
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
expect(keyInfo.algorithm)
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({
@@ -581,6 +572,7 @@ describe("Secrets", function() {
return [keyId, secretStorageKeys[keyId]];
}
}
return null;
},
},
},
@@ -639,7 +631,7 @@ describe("Secrets", function() {
},
}),
]);
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
firstUse: false,
crossSigningVerifiedBefore: false,
keys: {
@@ -681,14 +673,13 @@ describe("Secrets", function() {
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
this.emit(ClientEvent.AccountData, event);
return {};
};
await alice.bootstrapSecretStorage({});
const backupKey = alice.getAccountData("m.megolm_backup.v1")
.getContent();
const backupKey = alice.getAccountData("m.megolm_backup.v1")!.getContent();
expect(backupKey.encrypted).toHaveProperty("key_id");
expect(await alice.getSecret("m.megolm_backup.v1"))
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
@@ -13,9 +13,9 @@ 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 { MatrixClient } from "../../../../src/client";
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { MatrixEvent } from "../../../../src/models/event";
"../../../../src/crypto/verification/request/ToDeviceChannel";
describe("InRoomChannel tests", function() {
const ALICE = "@alice:hs.tld";
@@ -23,7 +23,7 @@ describe("InRoomChannel tests", function() {
const MALORY = "@malory:hs.tld";
const client = {
getUserId() { return ALICE; },
};
} as unknown as MatrixClient;
it("getEventType only returns .request for a message with a msgtype", function() {
const invalidEvent = new MatrixEvent({
@@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../../olm-loader";
import { verificationMethods } from "../../../../src/crypto";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import { logger } from "../../../../src/logger";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
import { makeTestClients } from './util';
const Olm = global.Olm;
@@ -31,14 +31,9 @@ describe("verification request integration tests with crypto layer", function()
}
beforeAll(function() {
setupWebcrypto();
return Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("should request and accept a verification", async function() {
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
@@ -49,26 +44,25 @@ describe("verification request integration tests with crypto layer", function()
verificationMethods: [verificationMethods.SAS],
},
);
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
alice.client.crypto!.deviceList.getRawStoredDevicesForUser = function() {
return {
Dynabook: {
algorithms: [],
verified: 0,
known: false,
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
};
};
alice.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.on("crypto.verification.request", (request) => {
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay)
// @ts-ignore Private function access (but it's a test, so we're okay)
bobVerifier.endTimer();
});
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
@@ -76,7 +70,7 @@ describe("verification request integration tests with crypto layer", function()
const aliceVerifier = aliceRequest.verifier;
expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay)
// @ts-ignore Private function access (but it's a test, so we're okay)
aliceVerifier.endTimer();
alice.stop();
@@ -15,14 +15,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../../olm-loader";
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
import { makeTestClients } from './util';
import { MatrixEvent } from "../../../../src/models/event";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { verificationMethods } from "../../../../src/crypto";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib";
import { logger } from "../../../../src/logger";
import { resetCrossSigningKeys } from "../crypto-utils";
import { VerificationBase as Verification, VerificationBase } from "../../../../src/crypto/verification/Base";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { MatrixClient } from "../../../../src";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
import { TestClient } from "../../../TestClient";
const Olm = global.Olm;
@@ -36,25 +41,22 @@ describe("SAS verification", function() {
}
beforeAll(function() {
setupWebcrypto();
return Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("should error on an unexpected event", async function() {
//channel, baseApis, userId, deviceId, startEvent, request
const request = {
onVerifierCancelled: function() {},
};
} as VerificationRequest;
const channel = {
send: function() {
return Promise.resolve();
},
};
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
} as unknown as IVerificationChannel;
const mockClient = {} as unknown as MatrixClient;
const event = new MatrixEvent({ type: 'test' });
const sas = new SAS(channel, mockClient, "@alice:example.com", "ABCDEFG", event, request);
sas.handleEvent(new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
@@ -65,17 +67,17 @@ describe("SAS verification", function() {
expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel();
sas.cancel(new Error('error'));
});
describe("verification", () => {
let alice;
let bob;
let aliceSasEvent;
let bobSasEvent;
let aliceVerifier;
let bobPromise;
let clearTestClientTimeouts;
let alice: TestClient;
let bob: TestClient;
let aliceSasEvent: ISasEvent | null;
let bobSasEvent: ISasEvent | null;
let aliceVerifier: Verification<any, any>;
let bobPromise: Promise<VerificationBase<any, any>>;
let clearTestClientTimeouts: () => void;
beforeEach(async () => {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
@@ -88,8 +90,8 @@ describe("SAS verification", function() {
},
);
const aliceDevice = alice.client.crypto.olmDevice;
const bobDevice = bob.client.crypto.olmDevice;
const aliceDevice = alice.client.crypto!.olmDevice;
const bobDevice = bob.client.crypto!.olmDevice;
ALICE_DEVICES = {
Osborne2: {
@@ -115,26 +117,26 @@ describe("SAS verification", function() {
},
};
alice.client.crypto.deviceList.storeDevicesForUser(
alice.client.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
alice.client.downloadKeys = () => {
return Promise.resolve();
return Promise.resolve({});
};
bob.client.crypto.deviceList.storeDevicesForUser(
bob.client.crypto!.deviceList.storeDevicesForUser(
"@alice:example.com", ALICE_DEVICES,
);
bob.client.downloadKeys = () => {
return Promise.resolve();
return Promise.resolve({});
};
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.client.on("crypto.verification.request", request => {
request.verifier.on("show_sas", (e) => {
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier!.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
@@ -150,14 +152,14 @@ describe("SAS verification", function() {
}
}
});
resolve(request.verifier);
resolve(request.verifier!);
});
});
aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId(), bob.deviceId,
verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!,
);
aliceVerifier.on("show_sas", (e) => {
aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
@@ -189,9 +191,9 @@ describe("SAS verification", function() {
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code;
keyAgreement = map[alice.client.getUserId()][alice.client.deviceId]
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!]
.key_agreement_protocol;
}
return origSendToDevice(type, map);
@@ -213,26 +215,26 @@ describe("SAS verification", function() {
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(),
bob.httpBackend.flush(),
alice.httpBackend.flush(undefined),
bob.httpBackend.flush(undefined),
]);
// make sure that it uses the preferred method
expect(macMethod).toBe("hkdf-hmac-sha256");
expect(macMethod).toBe("org.matrix.msc3783.hkdf-hmac-sha256");
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
// make sure Alice and Bob verified each other
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy();
expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy();
expect(aliceDevice?.isVerified()).toBeTruthy();
});
it("should be able to verify using the old MAC", async () => {
// pretend that Alice can only understand the old (incorrect) MAC,
// and make sure that she can still verify with Bob
it("should be able to verify using the old base64", async () => {
// pretend that Alice can only understand the old (incorrect) base64
// encoding, and make sure that she can still verify with Bob
let macMethod;
const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client);
alice.client.sendToDevice = (type, map) => {
@@ -242,15 +244,15 @@ 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[bob.client.getUserId()!][bob.client.deviceId!]
.message_authentication_codes = ['hkdf-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]
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code;
}
return bobOrigSendToDevice(type, map);
@@ -272,18 +274,74 @@ describe("SAS verification", function() {
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(),
bob.httpBackend.flush(),
alice.httpBackend.flush(undefined),
bob.httpBackend.flush(undefined),
]);
expect(macMethod).toBe("hkdf-hmac-sha256");
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice!.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice!.isVerified()).toBeTruthy();
});
it("should be able to verify using the old MAC", async () => {
// pretend that Alice can only understand the old (incorrect) MAC,
// and make sure that she can still verify with Bob
let macMethod;
const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client);
alice.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.start") {
// Note: this modifies not only the message that Bob
// receives, but also the copy of the message that Alice
// 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'];
}
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;
}
return bobOrigSendToDevice(type, map);
};
alice.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
},
});
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(undefined),
bob.httpBackend.flush(undefined),
]);
expect(macMethod).toBe("hmac-sha256");
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy();
expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy();
expect(aliceDevice?.isVerified()).toBeTruthy();
});
it("should verify a cross-signing key", async () => {
@@ -299,9 +357,11 @@ describe("SAS verification", function() {
await resetCrossSigningKeys(bob.client);
bob.client.crypto.deviceList.storeCrossSigningForUser(
bob.client.crypto!.deviceList.storeCrossSigningForUser(
"@alice:example.com", {
keys: alice.client.crypto.crossSigningInfo.keys,
keys: alice.client.crypto!.crossSigningInfo.keys,
crossSigningVerifiedBefore: false,
firstUse: true,
},
);
@@ -347,25 +407,21 @@ describe("SAS verification", function() {
},
);
alice.client.setDeviceVerified = jest.fn();
alice.client.downloadKeys = () => {
return Promise.resolve();
};
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
bob.client.setDeviceVerified = jest.fn();
bob.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
const bobPromise = new Promise((resolve, reject) => {
bob.client.on("crypto.verification.request", request => {
request.verifier.on("show_sas", (e) => {
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier!.on("show_sas", (e) => {
e.mismatch();
});
resolve(request.verifier);
resolve(request.verifier!);
});
});
const aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId,
verificationMethods.SAS, bob.client.getUserId()!, bob.client.deviceId!,
);
const aliceSpy = jest.fn();
@@ -406,7 +462,7 @@ describe("SAS verification", function() {
},
);
alice.client.setDeviceVerified = jest.fn();
alice.client.crypto!.setDeviceVerification = jest.fn();
alice.client.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
@@ -424,7 +480,7 @@ describe("SAS verification", function() {
return Promise.resolve();
};
bob.client.setDeviceVerified = jest.fn();
bob.client.crypto!.setDeviceVerification = jest.fn();
bob.client.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
@@ -445,7 +501,7 @@ describe("SAS verification", function() {
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bobPromise = new Promise<void>((resolve, reject) => {
bob.client.on("crypto.verification.request", async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME);
verifier.on("show_sas", (e) => {
@@ -507,10 +563,24 @@ describe("SAS verification", function() {
]);
// make sure Alice and Bob verified each other
expect(alice.client.setDeviceVerified)
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
expect(bob.client.setDeviceVerified)
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
expect(alice.client.crypto!.setDeviceVerification)
.toHaveBeenCalledWith(
bob.client.getUserId(),
bob.client.deviceId,
true,
null,
null,
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
);
expect(bob.client.crypto!.setDeviceVerification)
.toHaveBeenCalledWith(
alice.client.getUserId(),
alice.client.deviceId,
true,
null,
null,
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
);
});
});
});
@@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
import '../../../olm-loader';
import { MatrixClient, MatrixEvent } from '../../../../src/matrix';
import { encodeBase64 } from "../../../../src/crypto/olmlib";
import { setupWebcrypto, teardownWebcrypto } from './util';
import "../../../../src/crypto"; // import this to cycle-break
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
import { VerificationBase } from '../../../../src/crypto/verification/Base';
jest.useFakeTimers();
@@ -32,14 +36,9 @@ const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("self-verifications", () => {
beforeAll(function() {
setupWebcrypto();
return global.Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("triggers a request for key sharing upon completion", async () => {
const userId = "@test:localhost";
@@ -54,9 +53,21 @@ describe("self-verifications", () => {
cacheCallbacks,
);
crossSigningInfo.keys = {
master: { keys: { X: testKeyPub } },
self_signing: { keys: { X: testKeyPub } },
user_signing: { keys: { X: testKeyPub } },
master: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
self_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
user_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
};
const secretStorage = {
@@ -79,20 +90,22 @@ describe("self-verifications", () => {
getUserId: () => userId,
getKeyBackupVersion: () => Promise.resolve({}),
restoreKeyBackupWithCache,
};
} as unknown as MatrixClient;
const request = {
onVerifierFinished: () => undefined,
};
} as unknown as VerificationRequest;
const verification = new VerificationBase(
undefined, // channel
undefined as unknown as IVerificationChannel, // channel
client, // baseApis
userId,
"ABC", // deviceId
undefined, // startEvent
undefined as unknown as MatrixEvent, // startEvent
request,
);
// @ts-ignore set private property
verification.resolve = () => undefined;
const result = await verification.done();
@@ -102,12 +115,12 @@ describe("self-verifications", () => {
expect(secretStorage.request.mock.calls.length).toBe(4);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
.toEqual(testKey);
.toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
.toEqual(testKey);
.toEqual(testKey);
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
.toEqual(testKey);
.toEqual(testKey);
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
@@ -15,45 +15,47 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import nodeCrypto from "crypto";
import { TestClient } from '../../../TestClient';
import { 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 } from '../../../../src/client';
export async function makeTestClients(userInfos, options) {
const clients = [];
const timeouts = [];
const clientMap = {};
const sendToDevice = function(type, map) {
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
const clients: TestClient[] = [];
const timeouts: ReturnType<typeof setTimeout>[] = [];
const clientMap: Record<string, Record<string, MatrixClient>> = {};
const makeSendToDevice = (matrixClient: MatrixClient): MatrixClient['sendToDevice'] => async (type, map) => {
// logger.log(this.getUserId(), "sends", type, map);
for (const [userId, devMap] of Object.entries(map)) {
if (userId in clientMap) {
for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
sender: matrixClient.getUserId()!,
type: type,
content: msg,
});
const client = clientMap[userId][deviceId];
const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client.crypto) :
event.attemptDecryption(client.crypto!) :
Promise.resolve();
decryptionPromise.then(
() => client.emit("toDeviceEvent", event),
() => client.emit(ClientEvent.ToDeviceEvent, event),
);
}
}
}
}
return {};
};
const sendEvent = function(room, type, content) {
const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this
const eventId = "$" + matrixClient.makeTxnId();
const rawEvent = {
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
sender: matrixClient.getUserId()!,
type: type,
content: content,
room_id: room,
@@ -63,22 +65,24 @@ export async function makeTestClients(userInfos, options) {
const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
unsigned: {
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this
transaction_id: matrixClient.makeTxnId(),
},
}));
const timeout = setTimeout(() => {
for (const tc of clients) {
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
const room = new Room('test', tc.client, tc.client.getUserId()!);
const roomTimelineData = {} as unknown as IRoomTimelineData;
if (tc.client === matrixClient) {
logger.log("sending remote echo!!");
tc.client.emit("Room.timeline", remoteEcho);
tc.client.emit(RoomEvent.Timeline, remoteEcho, room, false, false, roomTimelineData);
} else {
tc.client.emit("Room.timeline", event);
tc.client.emit(RoomEvent.Timeline, event, room, false, false, roomTimelineData);
}
}
});
timeouts.push(timeout);
timeouts.push(timeout as unknown as ReturnType<typeof setTimeout>);
return Promise.resolve({ event_id: eventId });
};
@@ -99,8 +103,8 @@ export async function makeTestClients(userInfos, options) {
clientMap[userInfo.userId] = {};
}
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
testClient.client.sendToDevice = sendToDevice;
testClient.client.sendEvent = sendEvent;
testClient.client.sendToDevice = makeSendToDevice(testClient.client);
testClient.client.sendEvent = makeSendEvent(testClient.client);
clients.push(testClient);
}
@@ -112,15 +116,3 @@ export async function makeTestClients(userInfos, options) {
return [clients, destroy];
}
export function setupWebcrypto() {
global.crypto = {
getRandomValues: (buf) => {
return nodeCrypto.randomFillSync(buf);
},
};
}
export function teardownWebcrypto() {
global.crypto = undefined;
}
@@ -19,11 +19,17 @@ import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoo
import { ToDeviceChannel } from
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event";
import { setupWebcrypto, teardownWebcrypto } from "./util";
import { MatrixClient } from "../../../../src/client";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
function makeMockClient(userId, deviceId) {
type MockClient = MatrixClient & {
popEvents: () => MatrixEvent[];
popDeviceEvents: (userId: string, deviceId: string) => MatrixEvent[];
};
function makeMockClient(userId: string, deviceId: string): MockClient {
let counter = 1;
let events = [];
let events: MatrixEvent[] = [];
const deviceEvents = {};
return {
getUserId() { return userId; },
@@ -54,16 +60,18 @@ function makeMockClient(userId, deviceId) {
deviceEvents[userId][deviceId].push(event);
}
}
return Promise.resolve();
return Promise.resolve({});
},
popEvents() {
// @ts-ignore special testing fn
popEvents(): MatrixEvent[] {
const e = events;
events = [];
return e;
},
popDeviceEvents(userId, deviceId) {
// @ts-ignore special testing fn
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
const forDevice = deviceEvents[userId];
const events = forDevice && forDevice[deviceId];
const result = events || [];
@@ -72,12 +80,21 @@ function makeMockClient(userId, deviceId) {
}
return result;
},
};
} as unknown as MockClient;
}
const MOCK_METHOD = "mock-verify";
class MockVerifier {
constructor(channel, client, userId, deviceId, startEvent) {
class MockVerifier extends VerificationBase<'', any> {
public _channel;
public _startEvent;
constructor(
channel: IVerificationChannel,
client: MatrixClient,
userId: string,
deviceId: string,
startEvent: MatrixEvent,
) {
super(channel, client, userId, deviceId, startEvent, {} as unknown as VerificationRequest);
this._channel = channel;
this._startEvent = startEvent;
}
@@ -113,32 +130,38 @@ function makeRemoteEcho(event) {
}));
}
async function distributeEvent(ownRequest, theirRequest, event) {
async function distributeEvent(
ownRequest: VerificationRequest,
theirRequest: VerificationRequest,
event: MatrixEvent,
): Promise<void> {
await ownRequest.channel.handleEvent(
makeRemoteEcho(event), ownRequest, true);
makeRemoteEcho(event),
ownRequest,
true,
);
await theirRequest.channel.handleEvent(event, theirRequest, true);
}
jest.useFakeTimers();
describe("verification request unit tests", function() {
beforeAll(function() {
setupWebcrypto();
});
afterAll(() => {
teardownWebcrypto();
});
it("transition from UNSENT to DONE through happy path", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const verificationMethods = new Map(
[[MOCK_METHOD, MockVerifier]],
) as unknown as Map<string, typeof VerificationBase>;
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([[MOCK_METHOD, MockVerifier]]), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
verificationMethods,
alice,
);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([[MOCK_METHOD, MockVerifier]]), bob);
verificationMethods,
bob,
);
expect(aliceRequest.invalid).toBe(true);
expect(bobRequest.invalid).toBe(true);
@@ -157,7 +180,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest.ready).toBe(true);
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
await verifier.start();
await (verifier as MockVerifier).start();
const [startEvent] = alice.popEvents();
expect(startEvent.getType()).toBe(START_TYPE);
await distributeEvent(aliceRequest, bobRequest, startEvent);
@@ -165,8 +188,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
expect(bobRequest.started).toBe(true);
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
await bobRequest.verifier.start();
await (bobRequest.verifier as MockVerifier).start();
const [bobDoneEvent] = bob.popEvents();
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
@@ -180,12 +202,20 @@ describe("verification request unit tests", function() {
it("methods only contains common methods", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceVerificationMethods = new Map(
[["c", function() {}], ["a", function() {}]],
) as unknown as Map<string, typeof VerificationBase>;
const bobVerificationMethods = new Map(
[["c", function() {}], ["b", function() {}]],
) as unknown as Map<string, typeof VerificationBase>;
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([["c", function() {}], ["a", function() {}]]), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
aliceVerificationMethods, alice);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([["c", function() {}], ["b", function() {}]]), bob);
bobVerificationMethods,
bob,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await distributeEvent(aliceRequest, bobRequest, requestEvent);
@@ -201,13 +231,22 @@ describe("verification request unit tests", function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice);
new InRoomChannel(alice, "!room", bob1.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bob1Request = new VerificationRequest(
new InRoomChannel(bob1, "!room"), new Map(), bob1);
new InRoomChannel(bob1, "!room"),
new Map(),
bob1,
);
const bob2Request = new VerificationRequest(
new InRoomChannel(bob2, "!room"), new Map(), bob2);
new InRoomChannel(bob2, "!room"),
new Map(),
bob2,
);
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
@@ -222,22 +261,34 @@ describe("verification request unit tests", function() {
it("verify own device with to_device messages", async function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const verificationMethods = new Map(
[[MOCK_METHOD, MockVerifier]],
) as unknown as Map<string, typeof VerificationBase>;
const bob1Request = new VerificationRequest(
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
ToDeviceChannel.makeTransactionId(), "device2"),
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
new ToDeviceChannel(
bob1,
bob1.getUserId()!,
["device1", "device2"],
ToDeviceChannel.makeTransactionId(),
"device2",
),
verificationMethods,
bob1,
);
const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
expect(verifier).toBeInstanceOf(MockVerifier);
await verifier.start();
await (verifier as MockVerifier).start();
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
expect(startEvent.getType()).toBe(START_TYPE);
const bob2Request = new VerificationRequest(
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
new ToDeviceChannel(bob2, bob2.getUserId()!, ["device1"]),
verificationMethods,
bob2,
);
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
await bob2Request.verifier.start();
await (bob2Request.verifier as MockVerifier).start();
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
expect(doneEvent1.getType()).toBe(DONE_TYPE);
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
@@ -253,11 +304,13 @@ describe("verification request unit tests", function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
true, true);
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true);
expect(aliceRequest.cancelled).toBe(false);
expect(aliceRequest._cancellingUserId).toBe(undefined);
@@ -269,11 +322,17 @@ describe("verification request unit tests", function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"), new Map(), bob);
new InRoomChannel(bob, "!room"),
new Map(),
bob,
);
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
+331
View File
@@ -0,0 +1,331 @@
/**
* @jest-environment jsdom
*/
/*
Copyright 2022 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.
*/
// We have to use EventEmitter here to mock part of the matrix-widget-api
// project, which doesn't know about our TypeEventEmitter implementation at all
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { MockedObject } from "jest-mock";
import {
WidgetApi,
WidgetApiToWidgetAction,
MatrixCapabilities,
ITurnServer,
IRoomEvent,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded";
import { MatrixEvent } from "../../src/models/event";
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
class MockWidgetApi extends EventEmitter {
public start = jest.fn();
public requestCapability = jest.fn();
public requestCapabilities = jest.fn();
public requestCapabilityForRoomTimeline = jest.fn();
public requestCapabilityToSendEvent = jest.fn();
public requestCapabilityToReceiveEvent = jest.fn();
public requestCapabilityToSendMessage = jest.fn();
public requestCapabilityToReceiveMessage = jest.fn();
public requestCapabilityToSendState = jest.fn();
public requestCapabilityToReceiveState = jest.fn();
public requestCapabilityToSendToDevice = jest.fn();
public requestCapabilityToReceiveToDevice = jest.fn();
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
public sendStateEvent = jest.fn();
public sendToDevice = jest.fn();
public readStateEvents = jest.fn(() => []);
public getTurnServers = jest.fn(() => []);
public transport = { reply: jest.fn() };
}
describe("RoomWidgetClient", () => {
let widgetApi: MockedObject<WidgetApi>;
let client: MatrixClient;
beforeEach(() => {
widgetApi = new MockWidgetApi() as unknown as MockedObject<WidgetApi>;
});
afterEach(() => {
client.stopClient();
});
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
const baseUrl = "https://example.org";
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
widgetApi.emit("ready");
await client.startClient();
};
describe("events", () => {
it("sends", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
await client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 });
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request", { request_id: 123 }, "!1:example.org",
);
});
it("receives", async () => {
const event = new MatrixEvent({
type: "org.matrix.rageshake_request",
event_id: "$pduhfiidph",
room_id: "!1:example.org",
sender: "@alice:example.org",
content: { request_id: 123 },
}).getEffectiveEvent();
await makeClient({ receiveEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
);
// The client should've emitted about the received event
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
expect(await emittedSync).toEqual(SyncState.Syncing);
// It should've also inserted the event into the room object
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.getLiveTimeline().getEvents().map(e => e.getEffectiveEvent())).toEqual([event]);
});
});
describe("messages", () => {
it("requests permissions for specific message types", async () => {
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith(MsgType.Text);
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith(MsgType.Text);
});
it("requests permissions for all message types", async () => {
await makeClient({ sendMessage: true, receiveMessage: true });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith();
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
});
// No point in testing sending and receiving since it's done exactly the
// same way as non-message events
});
describe("state events", () => {
const event = new MatrixEvent({
type: "org.example.foo",
event_id: "$sfkjfsksdkfsd",
room_id: "!1:example.org",
sender: "@alice:example.org",
state_key: "bar",
content: { hello: "world" },
}).getEffectiveEvent();
it("sends", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar");
await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar");
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
"org.example.foo", "bar", { hello: "world" }, "!1:example.org",
);
});
it("receives", async () => {
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
);
// The client should've emitted about the received event
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
expect(await emittedSync).toEqual(SyncState.Syncing);
// It should've also inserted the event into the room object
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event);
});
it("backfills", async () => {
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
? [event as IRoomEvent]
: [],
);
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event);
});
});
describe("to-device messages", () => {
const unencryptedContentMap = {
"@alice:example.org": { "*": { hello: "alice!" } },
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
};
it("sends unencrypted (sendToDevice)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
await client.sendToDevice("org.example.foo", unencryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
});
it("sends unencrypted (queueToDevice)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
const batch: ToDeviceBatch = {
eventType: "org.example.foo",
batch: [
{ userId: "@alice:example.org", deviceId: "*", payload: { hello: "alice!" } },
{ userId: "@bob:example.org", deviceId: "bobDesktop", payload: { hello: "bob!" } },
],
};
await client.queueToDevice(batch);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
});
it("sends encrypted (encryptAndSendToDevices)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
const payload = { type: "org.example.foo", hello: "world" };
await client.encryptAndSendToDevices(
[
{ userId: "@alice:example.org", deviceInfo: new DeviceInfo("aliceWeb") },
{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobDesktop") },
],
payload,
);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
"@alice:example.org": { aliceWeb: payload },
"@bob:example.org": { bobDesktop: payload },
});
});
it.each([
{ encrypted: false, title: "unencrypted" },
{ encrypted: true, title: "encrypted" },
])("receives $title", async ({ encrypted }) => {
await makeClient({ receiveToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToReceiveToDevice).toHaveBeenCalledWith("org.example.foo");
const event = {
type: "org.example.foo",
sender: "@alice:example.org",
encrypted,
content: { hello: "world" },
};
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.ToDeviceEvent, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendToDevice}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }),
);
expect((await emittedEvent).getEffectiveEvent()).toEqual({
type: event.type,
sender: event.sender,
content: event.content,
});
expect((await emittedEvent).isEncrypted()).toEqual(encrypted);
expect(await emittedSync).toEqual(SyncState.Syncing);
});
});
it("gets TURN servers", async () => {
const server1: ITurnServer = {
uris: [
"turn:turn.example.com:3478?transport=udp",
"turn:10.20.30.40:3478?transport=tcp",
"turns:10.20.30.40:443?transport=tcp",
],
username: "1443779631:@user:example.com",
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
};
const server2: ITurnServer = {
uris: [
"turn:turn.example.com:3478?transport=udp",
"turn:10.20.30.40:3478?transport=tcp",
"turns:10.20.30.40:443?transport=tcp",
],
username: "1448999322:@user:example.com",
password: "hunter2",
};
const clientServer1: IClientTurnServer = {
urls: server1.uris,
username: server1.username,
credential: server1.password,
};
const clientServer2: IClientTurnServer = {
urls: server2.uris,
username: server2.username,
credential: server2.password,
};
let emitServer2: () => void;
const getServer2 = new Promise<ITurnServer>(resolve => emitServer2 = () => resolve(server2));
widgetApi.getTurnServers.mockImplementation(async function* () {
yield server1;
yield await getServer2;
});
await makeClient({ turnServers: true });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC3846TurnServers);
// The first server should've arrived immediately
expect(client.getTurnServers()).toEqual([clientServer1]);
// Subsequent servers arrive asynchronously and should emit an event
const emittedServer = new Promise<IClientTurnServer[]>(resolve =>
client.once(ClientEvent.TurnServers, resolve),
);
emitServer2!();
expect(await emittedServer).toEqual([clientServer2]);
expect(client.getTurnServers()).toEqual([clientServer2]);
});
});
+2 -2
View File
@@ -29,10 +29,10 @@ describe("eventMapperFor", function() {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: function() {} as any, // NOP
fetchFn: function() {} as any, // NOP
store: {
getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId);
return rooms.find(r => r.roomId === roomId) ?? null;
},
} as IStore,
scheduler: {
+115 -8
View File
@@ -16,14 +16,15 @@ limitations under the License.
import * as utils from "../test-utils/test-utils";
import {
DuplicateStrategy,
EventTimeline,
EventTimelineSet,
EventType,
Filter,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
Room,
DuplicateStrategy,
} from '../../src';
import { Thread } from "../../src/models/thread";
import { ReEmitter } from "../../src/ReEmitter";
@@ -44,16 +45,33 @@ describe('EventTimelineSet', () => {
it('should return the related events', () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId(),
messageEvent.getId()!,
"m.in_reply_to",
EventType.RoomMessage,
);
expect(relations).toBeDefined();
expect(relations.getRelations().length).toBe(1);
expect(relations.getRelations()[0].getId()).toBe(replyEvent.getId());
expect(relations!.getRelations().length).toBe(1);
expect(relations!.getRelations()[0].getId()).toBe(replyEvent.getId());
});
};
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}, room.client);
beforeEach(() => {
client = utils.mock(MatrixClient, 'MatrixClient');
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter');
@@ -116,6 +134,13 @@ describe('EventTimelineSet', () => {
});
describe('addEventToTimeline', () => {
let thread: Thread;
beforeEach(() => {
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
it("Adds event to timeline", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
@@ -143,6 +168,58 @@ describe('EventTimelineSet', () => {
);
}).not.toThrow();
});
it("should not add an event to a timeline that does not belong to the timelineSet", () => {
const eventTimelineSet2 = new EventTimelineSet(room);
const liveTimeline2 = eventTimelineSet2.getLiveTimeline();
expect(liveTimeline2.getEvents().length).toStrictEqual(0);
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
toStartOfTimeline: true,
});
}).toThrowError();
});
it("should not add a threaded reply to the main room timeline", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
const threadedReplyEvent = mkThreadResponse(messageEvent);
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
it("should not add a normal message to the timelineSet representing a thread", () => {
const eventTimelineSetForThread = new EventTimelineSet(room, {}, client, thread);
const liveTimeline = eventTimelineSetForThread.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
describe('non-room timeline', () => {
it('Adds event to timeline', () => {
const nonRoomEventTimelineSet = new EventTimelineSet(
// This is what we're specifically testing against, a timeline
// without a `room` defined
undefined,
);
const nonRoomEventTimeline = new EventTimeline(nonRoomEventTimelineSet);
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
toStartOfTimeline: true,
});
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
});
});
});
describe('aggregateRelations', () => {
@@ -192,7 +269,7 @@ describe('EventTimelineSet', () => {
it('should not return the related events', () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId(),
messageEvent.getId()!,
"m.in_reply_to",
EventType.RoomMessage,
);
@@ -235,7 +312,7 @@ describe('EventTimelineSet', () => {
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
"event_id": root.getId()!,
},
"rel_type": "m.thread",
},
@@ -277,18 +354,48 @@ describe('EventTimelineSet', () => {
});
it("should return true if the timeline set is for a thread and the event is its thread root", () => {
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
const thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
messageEvent.setThread(thread);
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
});
it("should return true if the timeline set is for a thread and the event is a response to it", () => {
const thread = new Thread(messageEvent.getId(), messageEvent, { room, client });
const thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
const eventTimelineSet = new EventTimelineSet(room, {}, client, thread);
messageEvent.setThread(thread);
const event = mkThreadResponse(messageEvent);
expect(eventTimelineSet.canContain(event)).toBeTruthy();
});
});
describe("handleRemoteEcho", () => {
it("should add to liveTimeline only if the event matches the filter", () => {
const filter = new Filter(client.getUserId()!, "test_filter");
filter.setDefinition({
room: {
timeline: {
types: [EventType.RoomMessage],
},
},
});
const eventTimelineSet = new EventTimelineSet(room, { filter }, client);
const roomMessageEvent = new MatrixEvent({
type: EventType.RoomMessage,
content: { body: "test" },
event_id: "!test1:server",
});
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId()!);
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
const roomFilteredEvent = new MatrixEvent({
type: "other_event_type",
content: { body: "test" },
event_id: "!test2:server",
});
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId()!);
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
});
});
});
@@ -1,26 +1,41 @@
import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
import { mocked } from 'jest-mock';
function mockRoomStates(timeline) {
timeline.startState = utils.mock(RoomState, "startState");
timeline.endState = utils.mock(RoomState, "endState");
}
import * as utils from "../test-utils/test-utils";
import { Direction, EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
import { MatrixClient } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { RoomMember } from "../../src/models/room-member";
import { EventTimelineSet } from "../../src/models/event-timeline-set";
describe("EventTimeline", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
let timeline;
let timeline: EventTimeline;
const mockClient = {} as unknown as MatrixClient;
const getTimeline = (): EventTimeline => {
const room = new Room(roomId, mockClient, userA);
const timelineSet = new EventTimelineSet(room);
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
const timeline = new EventTimeline(timelineSet);
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
// otherwise the default member property values (e.g. paginationToken) will be incorrect
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
return timeline;
};
beforeEach(function() {
// XXX: this is a horrid hack; should use sinon or something instead to mock
const timelineSet = { room: { roomId: roomId } };
timelineSet.room.getUnfilteredTimelineSet = function() {
return timelineSet;
};
// reset any RoomState mocks
jest.resetAllMocks();
timeline = new EventTimeline(timelineSet);
timeline = getTimeline();
});
describe("construction", function() {
@@ -31,10 +46,6 @@ describe("EventTimeline", function() {
});
describe("initialiseState", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
it("should copy state events to start and end state", function() {
const events = [
utils.mkMembership({
@@ -48,11 +59,15 @@ describe("EventTimeline", function() {
}),
];
timeline.initialiseState(events);
expect(timeline.startState.setStateEvents).toHaveBeenCalledWith(
// @ts-ignore private prop
const timelineStartState = timeline.startState!;
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(
events,
{ timelineWasEmpty: undefined },
);
expect(timeline.endState.setStateEvents).toHaveBeenCalledWith(
// @ts-ignore private prop
const timelineEndState = timeline.endState!;
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(
events,
{ timelineWasEmpty: undefined },
);
@@ -88,7 +103,17 @@ describe("EventTimeline", function() {
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
});
it("setPaginationToken should set token", function() {
it("setPaginationToken should set token", function() {
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
});
it("should be able to store pagination tokens for mixed room timelines", () => {
const timelineSet = new EventTimelineSet(undefined);
const timeline = new EventTimeline(timelineSet);
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
@@ -103,8 +128,8 @@ describe("EventTimeline", function() {
});
it("setNeighbouringTimeline should set neighbour", function() {
const prev = { a: "a" };
const next = { b: "b" };
const prev = getTimeline();
const next = getTimeline();
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
@@ -112,8 +137,8 @@ describe("EventTimeline", function() {
});
it("setNeighbouringTimeline should throw if called twice", function() {
const prev = { a: "a" };
const next = { b: "b" };
const prev = getTimeline();
const next = getTimeline();
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow();
@@ -135,10 +160,6 @@ describe("EventTimeline", function() {
});
describe("addEvent", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
@@ -171,24 +192,22 @@ describe("EventTimeline", function() {
});
it("should set event.sender for new and old events", function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
const oldSentinel = new RoomMember(roomId, userA);
sentinel.name = "Old Alice";
sentinel.membership = "join";
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
@@ -212,43 +231,41 @@ describe("EventTimeline", function() {
});
it("should set event.target for new and old m.room.member events",
function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
function() {
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
const oldSentinel = new RoomMember(roomId, userA);
sentinel.name = "Old Alice";
sentinel.membership = "join";
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
});
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
});
timeline.addEvent(newEv, { toStartOfTimeline: false });
expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
expect(oldEv.target).toEqual(oldSentinel);
});
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
});
timeline.addEvent(newEv, { toStartOfTimeline: false });
expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
@@ -267,15 +284,15 @@ describe("EventTimeline", function() {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
not.toHaveBeenCalled();
});
@@ -296,21 +313,25 @@ describe("EventTimeline", function() {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true });
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
not.toHaveBeenCalled();
});
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState() })).not.toThrow();
// @ts-ignore stateContext is not a valid param
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
expect(() => timeline.addEvent(events[0],
{ toStartOfTimeline: false, roomState: new RoomState(roomId) },
)).not.toThrow();
});
});
@@ -335,11 +356,11 @@ describe("EventTimeline", function() {
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getEvents().length).toEqual(2);
let ev = timeline.removeEvent(events[0].getId());
let ev = timeline.removeEvent(events[0].getId()!);
expect(ev).toBe(events[0]);
expect(timeline.getEvents().length).toEqual(1);
ev = timeline.removeEvent(events[1].getId());
ev = timeline.removeEvent(events[1].getId()!);
expect(ev).toBe(events[1]);
expect(timeline.getEvents().length).toEqual(0);
});
@@ -351,11 +372,11 @@ describe("EventTimeline", function() {
expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[2].getId());
timeline.removeEvent(events[2].getId()!);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[1].getId());
timeline.removeEvent(events[1].getId()!);
expect(timeline.getEvents().length).toEqual(1);
expect(timeline.getBaseIndex()).toEqual(0);
});
@@ -364,14 +385,14 @@ describe("EventTimeline", function() {
// - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event",
function() {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.removeEvent(events[0].getId());
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[2], { toStartOfTimeline: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
function() {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.removeEvent(events[0].getId()!);
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[2], { toStartOfTimeline: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
});
});
-76
View File
@@ -1,76 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 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 { logger } from "../../src/logger";
import { MatrixEvent } from "../../src/models/event";
describe("MatrixEvent", () => {
describe(".attemptDecryption", () => {
let encryptedEvent;
beforeEach(() => {
encryptedEvent = new MatrixEvent({
id: 'test_encrypted_event',
type: 'm.room.encrypted',
content: {
ciphertext: 'secrets',
},
});
});
it('should retry decryption if a retry is queued', () => {
let callCount = 0;
let prom2;
let prom2Fulfilled = false;
const crypto = {
decryptEvent: function() {
++callCount;
logger.log(`decrypt: ${callCount}`);
if (callCount == 1) {
// schedule a second decryption attempt while
// the first one is still running.
prom2 = encryptedEvent.attemptDecryption(crypto);
prom2.then(() => prom2Fulfilled = true);
const error = new Error("nope");
error.name = 'DecryptionError';
return Promise.reject(error);
} else {
expect(prom2Fulfilled).toBe(
false, 'second attemptDecryption resolved too soon');
return Promise.resolve({
clearEvent: {
type: 'm.room.message',
},
});
}
},
};
return encryptedEvent.attemptDecryption(crypto).then(() => {
expect(callCount).toEqual(2);
expect(encryptedEvent.getType()).toEqual('m.room.message');
// make sure the second attemptDecryption resolves
return prom2;
});
});
});
});
+62
View File
@@ -0,0 +1,62 @@
/*
Copyright 2022 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 { buildFeatureSupportMap, Feature, ServerSupport } from "../../src/feature";
describe("Feature detection", () => {
it("checks the matrix version", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.3"],
unstable_features: {},
});
expect(support.get(Feature.Thread)).toBe(ServerSupport.Stable);
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
});
it("checks the matrix msc number", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.2"],
unstable_features: {
"org.matrix.msc3771": true,
"org.matrix.msc3773": true,
},
});
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unstable);
});
it("requires two MSCs to pass", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.2"],
unstable_features: {
"org.matrix.msc3771": false,
"org.matrix.msc3773": true,
},
});
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
});
it("requires two MSCs OR matrix versions to pass", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.4"],
unstable_features: {
"org.matrix.msc3771": false,
"org.matrix.msc3773": true,
},
});
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Stable);
});
});
-46
View File
@@ -1,46 +0,0 @@
import { Filter } from "../../src/filter";
describe("Filter", function() {
const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger";
let filter;
beforeEach(function() {
filter = new Filter(userId);
});
describe("fromJson", function() {
it("create a new Filter from the provided values", function() {
const definition = {
event_fields: ["type", "content"],
};
const f = Filter.fromJson(userId, filterId, definition);
expect(f.getDefinition()).toEqual(definition);
expect(f.userId).toEqual(userId);
expect(f.filterId).toEqual(filterId);
});
});
describe("setTimelineLimit", function() {
it("should set room.timeline.limit of the filter definition", function() {
filter.setTimelineLimit(10);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
limit: 10,
},
},
});
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
const definition = {
event_format: "client",
};
filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition);
});
});
});
+101
View File
@@ -0,0 +1,101 @@
/*
Copyright 2022 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 { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
import { Filter, IFilterDefinition } from "../../src/filter";
import { mkEvent } from "../test-utils/test-utils";
import { EventType } from "../../src";
describe("Filter", function() {
const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger";
let filter: Filter;
beforeEach(function() {
filter = new Filter(userId);
});
describe("fromJson", function() {
it("create a new Filter from the provided values", function() {
const definition = {
event_fields: ["type", "content"],
};
const f = Filter.fromJson(userId, filterId, definition);
expect(f.getDefinition()).toEqual(definition);
expect(f.userId).toEqual(userId);
expect(f.filterId).toEqual(filterId);
});
});
describe("setTimelineLimit", function() {
it("should set room.timeline.limit of the filter definition", function() {
filter.setTimelineLimit(10);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
limit: 10,
},
},
});
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
const definition = {
event_format: "client" as IFilterDefinition['event_format'],
};
filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition);
});
});
describe("setUnreadThreadNotifications", function() {
it("setUnreadThreadNotifications", function() {
filter.setUnreadThreadNotifications(true);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
[UNREAD_THREAD_NOTIFICATIONS.name]: true,
},
},
});
});
});
describe("filterRoomTimeline", () => {
it("should return input if no roomTimelineFilter and roomFilter", () => {
const events = [mkEvent({ type: EventType.Sticker, content: {}, event: true })];
expect(new Filter(undefined).filterRoomTimeline(events)).toStrictEqual(events);
});
it("should filter using components when present", () => {
const definition: IFilterDefinition = {
room: {
timeline: {
types: [EventType.Sticker],
},
},
};
const filter = Filter.fromJson(userId, filterId, definition);
const events = [
mkEvent({ type: EventType.Sticker, content: {}, event: true }),
mkEvent({ type: EventType.RoomMessage, content: {}, event: true }),
];
expect(filter.filterRoomTimeline(events)).toStrictEqual([events[0]]);
});
});
});
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
{
"base": "http://baseUrl",
"params": {
"access_token": "token",
},
"path": "/_matrix/media/r0/upload",
}
`;
+233
View File
@@ -0,0 +1,233 @@
/*
Copyright 2022 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 { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
describe("FetchHttpApi", () => {
const baseUrl = "http://baseUrl";
const idBaseUrl = "http://idBaseUrl";
const prefix = ClientPrefix.V3;
it("should support aborting multiple times", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.request(Method.Get, "/foo");
api.request(Method.Get, "/baz");
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
api.request(Method.Get, "/bar");
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
});
it("should fall back to global fetch if fetchFn not provided", () => {
global.fetch = jest.fn();
expect(global.fetch).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
api.fetch("test");
expect(global.fetch).toHaveBeenCalled();
});
it("should update identity server base url", () => {
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(api.opts.idBaseUrl).toBeUndefined();
api.setIdBaseUrl("https://id.foo.bar");
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
});
describe("idServerRequest", () => {
it("should throw if no idBaseUrl", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2))
.toThrow("No identity server base URL set");
});
it("should send params as query string for GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
});
it("should send params as body for non-GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
const params = { foo: "bar", via: ["a", "b"] };
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
});
it("should add Authorization header if token provided", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
});
});
it("should return the Response object if onlyData=false", async () => {
const res = { ok: true };
const fetchFn = jest.fn().mockResolvedValue(res);
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
});
it("should return text if json=false", async () => {
const text = "418 I'm a teapot";
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(api.requestOtherUrl(Method.Get, "http://url", undefined, {
json: false,
})).resolves.toBe(text);
});
it("should send token via query params if useAuthorizationHeader=false", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
});
it("should send token via headers by default", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not send a token if not calling `authedRequest`", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.request(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
});
it("should ensure no token is leaked out via query params if sending via headers", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", { access_token: "123" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not override manually specified access token via query params", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
});
it("should not override manually specified access token via header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Authorization: "Bearer RealToken" },
});
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
});
it("should not override Accept header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Accept: "text/html" },
});
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
});
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
const fetchFn = jest.fn().mockResolvedValue({
ok: false,
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(JSON.stringify({
errcode: "M_CONSENT_NOT_GIVEN",
error: "Ye shall ask for consent",
})),
});
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
await Promise.all([
emitPromise(emitter, HttpApiEvent.NoConsent),
expect(api.authedRequest(Method.Get, "/path")).rejects.toThrow("Ye shall ask for consent"),
]);
});
describe("authedRequest", () => {
it("should not include token if unset", () => {
const fetchFn = jest.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Post, "/account/password");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
});
});
});
+228
View File
@@ -0,0 +1,228 @@
/*
Copyright 2022 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 DOMException from "domexception";
import { mocked } from "jest-mock";
import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
jest.useFakeTimers();
describe("MatrixHttpApi", () => {
const baseUrl = "http://baseUrl";
const prefix = ClientPrefix.V3;
let xhr: Writeable<XMLHttpRequest>;
let upload: Promise<UploadResponse>;
const DONE = 0;
global.DOMException = DOMException;
beforeEach(() => {
xhr = {
upload: {} as XMLHttpRequestUpload,
open: jest.fn(),
send: jest.fn(),
abort: jest.fn(),
setRequestHeader: jest.fn(),
onreadystatechange: undefined,
getResponseHeader: jest.fn(),
} as unknown as XMLHttpRequest;
// We stub out XHR here as it is not available in JSDOM
// @ts-ignore
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
// @ts-ignore
global.XMLHttpRequest.DONE = DONE;
});
afterEach(() => {
upload?.catch(() => {});
// Abort any remaining requests
xhr.readyState = DONE;
xhr.status = 0;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
});
it("should fall back to `fetch` where xhr is unavailable", () => {
global.XMLHttpRequest = undefined!;
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
expect(fetchFn).toHaveBeenCalled();
});
it("should prefer xhr where available", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
expect(fetchFn).not.toHaveBeenCalled();
expect(xhr.open).toHaveBeenCalled();
});
it("should send access token in query params if header disabled", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
accessToken: "token",
useAuthorizationHeader: false,
});
upload = api.uploadContent({} as File);
expect(xhr.open)
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token");
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
});
it("should send access token in header by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
accessToken: "token",
});
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
});
it("should include filename by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File, { name: "name" });
expect(xhr.open)
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name");
});
it("should allow not sending the filename", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
});
it("should abort xhr when the upload is aborted", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
api.cancelUpload(upload);
expect(xhr.abort).toHaveBeenCalled();
return expect(upload).rejects.toThrow("Aborted");
});
it("should timeout if no progress in 30s", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
jest.advanceTimersByTime(25000);
// @ts-ignore
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
jest.advanceTimersByTime(25000);
expect(xhr.abort).not.toHaveBeenCalled();
jest.advanceTimersByTime(5000);
expect(xhr.abort).toHaveBeenCalled();
});
it("should call progressHandler", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
const progressHandler = jest.fn();
upload = api.uploadContent({} as File, { progressHandler });
const progressEvent = new Event("progress") as ProgressEvent;
Object.assign(progressEvent, { loaded: 1, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
Object.assign(progressEvent, { loaded: 95, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
});
it("should error when no response body", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = "";
xhr.status = 200;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("No response body.");
});
it("should error on a 400-code", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
xhr.status = 404;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("Not found");
});
it("should return response on successful upload", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
xhr.status = 200;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
});
it("should abort xhr when calling `cancelUpload`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
expect(api.cancelUpload(upload)).toBeTruthy();
expect(xhr.abort).toHaveBeenCalled();
});
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.status = 500;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
await upload.catch(() => {});
expect(api.cancelUpload(upload)).toBeFalsy();
expect(xhr.abort).not.toHaveBeenCalled();
});
it("should return active uploads in `getCurrentUploads`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeTruthy();
api.cancelUpload(upload);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeFalsy();
});
it("should return expected object from `getContentUri`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, accessToken: "token" });
expect(api.getContentUri()).toMatchSnapshot();
});
});
+221
View File
@@ -0,0 +1,221 @@
/*
Copyright 2022 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 {
anySignal,
ConnectionError,
HTTPError,
MatrixError,
parseErrorResponse,
retryNetworkOperation,
timeoutSignal,
} from "../../../src";
import { sleep } from "../../../src/utils";
jest.mock("../../../src/utils");
// setupTests mocks `timeoutSignal` due to hanging timers
jest.unmock("../../../src/http-api/utils");
describe("timeoutSignal", () => {
jest.useFakeTimers();
it("should fire abort signal after specified timeout", () => {
const signal = timeoutSignal(3000);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(3000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
});
describe("anySignal", () => {
jest.useFakeTimers();
it("should fire when any signal fires", () => {
const { signal } = anySignal([
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(2000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
it("should cleanup when instructed", () => {
const { signal, cleanup } = anySignal([
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
cleanup();
jest.advanceTimersByTime(2000);
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
});
it("should abort immediately if passed an aborted signal", () => {
const controller = new AbortController();
controller.abort();
const { signal } = anySignal([controller.signal]);
expect(signal.aborted).toBeTruthy();
});
});
describe("parseErrorResponse", () => {
it("should resolve Matrix Errors from XHR", () => {
expect(parseErrorResponse({
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
status: 500,
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500));
});
it("should resolve Matrix Errors from fetch", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500));
});
it("should resolve Matrix Errors from XHR with urls", () => {
expect(parseErrorResponse({
responseURL: "https://example.com",
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
status: 500,
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500, "https://example.com"));
});
it("should resolve Matrix Errors from fetch with urls", () => {
expect(parseErrorResponse({
url: "https://example.com",
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500, "https://example.com"));
});
it("should set a sensible default error message on MatrixError", () => {
let err = new MatrixError();
expect(err.message).toEqual("MatrixError: Unknown message");
err = new MatrixError({
error: "Oh no",
});
expect(err.message).toEqual("MatrixError: Oh no");
});
it("should handle no type gracefully", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new HTTPError("Server returned 500 error", 500));
});
it("should handle invalid type gracefully", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? " " : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}'))
.toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
});
it("should handle plaintext errors", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "text/plain" : null;
},
},
status: 418,
} as Response, "I'm a teapot")).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418));
});
});
describe("retryNetworkOperation", () => {
it("should retry given number of times with exponential sleeps", async () => {
const err = new ConnectionError("test");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(4);
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
});
it("should bail out on errors other than ConnectionError", async () => {
const err = new TypeError("invalid JSON");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(1);
});
it("should return newest ConnectionError when giving up", async () => {
const err1 = new ConnectionError("test1");
const err2 = new ConnectionError("test2");
const err3 = new ConnectionError("test3");
const errors = [err1, err2, err3];
const fn = jest.fn().mockImplementation(() => {
throw errors.shift();
});
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
});
});
-280
View File
@@ -1,280 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 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 { logger } from "../../src/logger";
import { InteractiveAuth } from "../../src/interactive-auth";
import { MatrixError } from "../../src/http-api";
import { sleep } from "../../src/utils";
import { randomString } from "../../src/randomstring";
// Trivial client object to test interactive auth
// (we do not need TestClient here)
class FakeClient {
generateClientSecret() {
return "testcl1Ent5EcreT";
}
}
describe("InteractiveAuth", function() {
it("should start an auth stage and complete it", function() {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
authData: {
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation(function(stage) {
logger.log('aaaa');
expect(stage).toEqual("logintype");
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(function(authData) {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return Promise.resolve(requestRes);
});
return ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
});
it("should make a request if no authdata is provided", function() {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
stateUpdated: stateUpdated,
doRequest: doRequest,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams("logintype")).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation(function(authData) {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation(function(stage) {
expect(stage).toEqual("logintype");
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(function(authData) {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return Promise.resolve(requestRes);
});
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
return ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
});
it("should start an auth stage and reject if no auth flow", function() {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
});
doRequest.mockImplementation(function(authData) {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
"logintype": { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
return ia.attemptAuth().catch(function(error) {
expect(error.message).toBe('No appropriate authentication flow found');
});
});
describe("requestEmailToken", () => {
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: new FakeClient(),
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("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: new FakeClient(),
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();
const requestEmailToken = jest.fn();
requestEmailToken.mockImplementation(async () => {
throw new Error("unspecific network error");
});
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
expect(async () => await ia.requestEmailToken()).rejects.toThrowError("unspecific network error");
});
it("only starts one request at a time", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await Promise.all([ia.requestEmailToken(), ia.requestEmailToken(), ia.requestEmailToken()]);
expect(requestEmailToken).toHaveBeenCalledTimes(1);
});
it("stores result in email sid", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const sid = randomString(24);
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await ia.requestEmailToken();
expect(ia.getEmailSid()).toEqual(sid);
});
});
});
+546
View File
@@ -0,0 +1,546 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 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 { MatrixClient } from "../../src/client";
import { logger } from "../../src/logger";
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
import { HTTPError, MatrixError } from "../../src/http-api";
import { sleep } from "../../src/utils";
import { randomString } from "../../src/randomstring";
// Trivial client object to test interactive auth
// (we do not need TestClient here)
class FakeClient {
generateClientSecret() {
return "testcl1Ent5EcreT";
}
}
const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixClient;
describe("InteractiveAuth", () => {
it("should start an auth stage and complete it", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
params: {
[AuthType.Password]: { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('aaaa');
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
type: AuthType.Password,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should handle auth errcode presence ", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
requestEmailToken: jest.fn(),
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
errcode: "MockError0",
params: {
[AuthType.Password]: { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('aaaa');
expect(stage).toEqual(AuthType.Password);
ia.submitAuthDict({
type: AuthType.Password,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should handle set emailSid for email flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
doRequest,
stateUpdated,
requestEmailToken,
matrixClient: getFakeClient(),
emailSid: 'myEmailSid',
authData: {
session: "sessionId",
flows: [
{ stages: [AuthType.Email, AuthType.Password] },
],
params: {
[AuthType.Email]: { param: "aa" },
[AuthType.Password]: { param: "bb" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Email)).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.mockImplementation((stage) => {
logger.log('husky');
expect(stage).toEqual(AuthType.Email);
ia.submitAuthDict({
type: AuthType.Email,
});
});
// .. which should trigger a call here
const requestRes = { "a": "b" };
doRequest.mockImplementation(async (authData) => {
logger.log('barfoo');
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Email,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(1);
expect(requestEmailToken).toBeCalledTimes(0);
expect(ia.getEmailSid()).toBe("myEmailSid");
});
it("should make a request if no authdata is provided", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
stateUpdated,
doRequest,
requestEmailToken,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
params: {
[AuthType.Password]: { param: "aa" },
},
}, 401);
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
ia.submitAuthDict({
type: AuthType.Password,
});
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should make a request if authdata is null", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
stateUpdated,
doRequest,
requestEmailToken,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams(AuthType.Password)).toBe(undefined);
// first we expect a call to doRequest
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: [AuthType.Password] },
],
params: {
[AuthType.Password]: { param: "aa" },
},
}, 401);
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = { "a": "b" };
stateUpdated.mockImplementation((stage) => {
expect(stage).toEqual(AuthType.Password);
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams(AuthType.Password)).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.mockImplementation(async (authData) => {
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Password,
});
return requestRes;
});
ia.submitAuthDict({
type: AuthType.Password,
});
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(2);
expect(stateUpdated).toBeCalledTimes(1);
});
it("should start an auth stage and reject if no auth flow", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
[AuthType.Password]: { param: "aa" },
},
}, 401);
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error('No appropriate authentication flow found'),
);
});
it("should start an auth stage and reject if no auth flow but has session", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
},
sessionId: "sessionId",
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
[AuthType.Password]: { param: "aa" },
},
error: "Mock Error 1",
errcode: "MOCKERR1",
}, 401);
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error('No appropriate authentication flow found'),
);
});
it("should handle unexpected error types without data propery set", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: "sessionId",
},
});
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new HTTPError('myerror', 401);
throw err;
});
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
new Error("myerror"),
);
});
it("should allow dummy auth", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest,
stateUpdated,
requestEmailToken,
authData: {
session: 'sessionId',
flows: [
{ stages: [AuthType.Dummy] },
],
params: {},
},
});
const requestRes = { "a": "b" };
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({
session: "sessionId",
type: AuthType.Dummy,
});
return requestRes;
});
const res = await ia.attemptAuth();
expect(res).toBe(requestRes);
expect(doRequest).toBeCalledTimes(1);
expect(stateUpdated).toBeCalledTimes(0);
});
describe("requestEmailToken", () => {
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("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();
const requestEmailToken = jest.fn();
requestEmailToken.mockImplementation(async () => {
throw new Error("unspecific network error");
});
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
});
it("only starts one request at a time", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await Promise.all([ia.requestEmailToken(), ia.requestEmailToken(), ia.requestEmailToken()]);
expect(requestEmailToken).toHaveBeenCalledTimes(1);
});
it("stores result in email sid", async () => {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const requestEmailToken = jest.fn();
const sid = randomString(24);
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
const ia = new InteractiveAuth({
matrixClient: getFakeClient(),
doRequest, stateUpdated, requestEmailToken,
});
await ia.requestEmailToken();
expect(ia.getEmailSid()).toEqual(sid);
});
});
});
+43
View File
@@ -0,0 +1,43 @@
/*
Copyright 2022 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 { LocalNotificationSettings } from "../../src/@types/local_notifications";
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix";
import { TestClient } from '../TestClient';
let client: MatrixClient;
describe("Local notification settings", () => {
beforeEach(() => {
client = (new TestClient(
"@alice:matrix.org", "123", undefined, undefined, undefined,
)).client;
client.setAccountData = jest.fn();
});
describe("Lets you set local notification settings", () => {
it("stores settings in account data", () => {
const deviceId = "device";
const settings: LocalNotificationSettings = { is_silenced: true };
client.setLocalNotificationSettings(deviceId, settings);
expect(client.setAccountData).toHaveBeenCalledWith(
`${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`,
settings,
);
});
});
});
-24
View File
@@ -1,24 +0,0 @@
import { TestClient } from '../TestClient';
describe('Login request', function() {
let client;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
it('should store "access_token" and "user_id" if in response', async function() {
const response = { user_id: 1, access_token: Date.now().toString(16) };
client.httpBackend.when('POST', '/login').respond(200, response);
client.httpBackend.flush('/login', 1, 100);
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
expect(client.client.getAccessToken()).toBe(response.access_token);
expect(client.client.getUserId()).toBe(response.user_id);
});
});
+59
View File
@@ -0,0 +1,59 @@
import { SSOAction } from '../../src/@types/auth';
import { TestClient } from '../TestClient';
describe('Login request', function() {
let client: TestClient;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
it('should store "access_token" and "user_id" if in response', async function() {
const response = { user_id: 1, access_token: Date.now().toString(16) };
client.httpBackend.when('POST', '/login').respond(200, response);
client.httpBackend.flush('/login', 1, 100);
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
expect(client.client.getAccessToken()).toBe(response.access_token);
expect(client.client.getUserId()).toBe(response.user_id);
});
});
describe('SSO login URL', function() {
let client: TestClient;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
describe('SSOAction', function() {
const redirectUri = "https://test.com/foo";
it('No action', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
const url = new URL(urlString);
expect(url.searchParams.has('org.matrix.msc3824.action')).toBe(false);
});
it('register', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('register');
});
it('login', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('login');
});
});
});
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -312,7 +312,7 @@ describe("MSC3089Branch", () => {
} as MatrixEvent);
const events = [await branch.getFileEvent(), await branch2.getFileEvent(), {
replacingEventId: (): string => null,
replacingEventId: (): string | undefined => undefined,
getId: () => "$unknown",
}];
staticRoom.getLiveTimeline = () => ({ getEvents: () => events }) as EventTimeline;
+15 -17
View File
@@ -135,7 +135,7 @@ describe("MSC3089TreeSpace", () => {
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.errcode).toEqual("M_FORBIDDEN");
expect((<MatrixError>e).errcode).toEqual("M_FORBIDDEN");
}
expect(fn).toHaveBeenCalledTimes(1);
@@ -513,7 +513,7 @@ describe("MSC3089TreeSpace", () => {
function expectOrder(childRoomId: string, order: number) {
const child = childTrees.find(c => c.roomId === childRoomId);
expect(child).toBeDefined();
expect(child.getOrder()).toEqual(order);
expect(child!.getOrder()).toEqual(order);
}
function makeMockChildRoom(roomId: string): Room {
@@ -565,7 +565,7 @@ describe("MSC3089TreeSpace", () => {
rooms = {};
rooms[tree.roomId] = parentRoom;
(<any>tree).room = parentRoom; // override readonly
client.getRoom = (r) => rooms[r];
client.getRoom = (r) => rooms[r ?? ""];
clientSendStateFn = jest.fn()
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
@@ -608,7 +608,7 @@ describe("MSC3089TreeSpace", () => {
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect(e.message).toEqual("Cannot set order of top level spaces currently");
expect((<Error>e).message).toEqual("Cannot set order of top level spaces currently");
}
});
@@ -706,7 +706,7 @@ describe("MSC3089TreeSpace", () => {
const treeA = childTrees.find(c => c.roomId === a);
expect(treeA).toBeDefined();
await treeA.setOrder(1);
await treeA!.setOrder(1);
expect(clientSendStateFn).toHaveBeenCalledTimes(3);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -743,7 +743,7 @@ describe("MSC3089TreeSpace", () => {
const treeA = childTrees.find(c => c.roomId === a);
expect(treeA).toBeDefined();
await treeA.setOrder(1);
await treeA!.setOrder(1);
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -771,7 +771,7 @@ describe("MSC3089TreeSpace", () => {
const treeA = childTrees.find(c => c.roomId === a);
expect(treeA).toBeDefined();
await treeA.setOrder(2);
await treeA!.setOrder(2);
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -800,7 +800,7 @@ describe("MSC3089TreeSpace", () => {
const treeB = childTrees.find(c => c.roomId === b);
expect(treeB).toBeDefined();
await treeB.setOrder(2);
await treeB!.setOrder(2);
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -829,7 +829,7 @@ describe("MSC3089TreeSpace", () => {
const treeC = childTrees.find(ch => ch.roomId === c);
expect(treeC).toBeDefined();
await treeC.setOrder(1);
await treeC!.setOrder(1);
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -858,7 +858,7 @@ describe("MSC3089TreeSpace", () => {
const treeB = childTrees.find(ch => ch.roomId === b);
expect(treeB).toBeDefined();
await treeB.setOrder(2);
await treeB!.setOrder(2);
expect(clientSendStateFn).toHaveBeenCalledTimes(2);
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
@@ -890,9 +890,8 @@ describe("MSC3089TreeSpace", () => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
});
return Promise.resolve(mxc);
return Promise.resolve({ content_uri: mxc });
});
client.uploadContent = uploadFn;
@@ -904,7 +903,7 @@ describe("MSC3089TreeSpace", () => {
url: mxc,
file: fileInfo,
metadata: true, // additional content from test
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
});
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
@@ -950,9 +949,8 @@ describe("MSC3089TreeSpace", () => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
});
return Promise.resolve(mxc);
return Promise.resolve({ content_uri: mxc });
});
client.uploadContent = uploadFn;
@@ -967,7 +965,7 @@ describe("MSC3089TreeSpace", () => {
expect(contents).toMatchObject({
...content,
"m.new_content": content,
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
});
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
@@ -1012,7 +1010,7 @@ describe("MSC3089TreeSpace", () => {
const file = tree.getFile(fileEventId);
expect(file).toBeDefined();
expect(file.indexEvent).toBe(fileEvent);
expect(file!.indexEvent).toBe(fileEvent);
});
it('should return falsy for unknown files', () => {
+43 -1
View File
@@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { MatrixEvent } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon";
import {
isTimestampInDuration,
Beacon,
@@ -129,6 +132,24 @@ describe('Beacon', () => {
expect(beacon.beaconInfo).toBeTruthy();
});
it('creates beacon without error from a malformed event', () => {
const event = new MatrixEvent({
type: M_BEACON_INFO.name,
room_id: roomId,
state_key: userId,
content: {},
});
const beacon = new Beacon(event);
expect(beacon.beaconInfoId).toEqual(event.getId());
expect(beacon.roomId).toEqual(roomId);
expect(beacon.isLive).toEqual(false);
expect(beacon.beaconInfoOwner).toEqual(userId);
expect(beacon.beaconInfoEventType).toEqual(liveBeaconEvent.getType());
expect(beacon.identifier).toEqual(`${roomId}_${userId}`);
expect(beacon.beaconInfo).toBeTruthy();
});
describe('isLive()', () => {
it('returns false when beacon is explicitly set to not live', () => {
const beacon = new Beacon(notLiveBeaconEvent);
@@ -242,7 +263,7 @@ describe('Beacon', () => {
roomId,
);
// less than the original event
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts - 1000;
oldUpdateEvent.event.origin_server_ts = liveBeaconEvent.event.origin_server_ts! - 1000;
beacon.update(oldUpdateEvent);
// didnt update
@@ -412,6 +433,27 @@ describe('Beacon', () => {
expect(emitSpy).not.toHaveBeenCalled();
});
it("should ignore invalid beacon events", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit');
const ev = new MatrixEvent({
type: M_BEACON_INFO.name,
sender: userId,
room_id: roomId,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beacon.beaconInfoId,
},
},
});
beacon.addLocations([ev]);
expect(beacon.latestLocationEvent).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
describe('when beacon is live with a start timestamp is in the future', () => {
it('ignores locations before the beacon start timestamp', () => {
const startTimestamp = now + 60000;
+81 -1
View File
@@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "../../../src/models/event";
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils";
import { EventType } from "../../../src";
import { Crypto } from "../../../src/crypto";
describe('MatrixEvent', () => {
it('should create copies of itself', () => {
@@ -84,4 +87,81 @@ describe('MatrixEvent', () => {
expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).toBeUndefined();
});
it("should abort decryption if fails with an error other than a DecryptionError", async () => {
const ev = new MatrixEvent({
type: EventType.RoomMessageEncrypted,
content: {
body: "Test",
},
event_id: "$event1:server",
});
await ev.attemptDecryption({
decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")),
} as unknown as Crypto);
expect(ev.isEncrypted()).toBeTruthy();
expect(ev.isBeingDecrypted()).toBeFalsy();
expect(ev.isDecryptionFailure()).toBeFalsy();
});
describe("applyVisibilityEvent", () => {
it("should emit VisibilityChange if a change was made", async () => {
const ev = new MatrixEvent({
type: "m.room.message",
content: {
body: "Test",
},
event_id: "$event1:server",
});
const prom = emitPromise(ev, MatrixEventEvent.VisibilityChange);
ev.applyVisibilityEvent({ visible: false, eventId: ev.getId()!, reason: null });
await prom;
});
});
describe(".attemptDecryption", () => {
let encryptedEvent;
const eventId = 'test_encrypted_event';
beforeEach(() => {
encryptedEvent = new MatrixEvent({
event_id: eventId,
type: 'm.room.encrypted',
content: {
ciphertext: 'secrets',
},
});
});
it('should retry decryption if a retry is queued', async () => {
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, 'attemptDecryption');
const crypto = {
decryptEvent: jest.fn()
.mockImplementationOnce(() => {
// schedule a second decryption attempt while
// the first one is still running.
encryptedEvent.attemptDecryption(crypto);
const error = new Error("nope");
error.name = 'DecryptionError';
return Promise.reject(error);
})
.mockImplementationOnce(() => {
return Promise.resolve({
clearEvent: {
type: 'm.room.message',
},
});
}),
};
await encryptedEvent.attemptDecryption(crypto);
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
expect(encryptedEvent.getType()).toEqual('m.room.message');
});
});
});

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