Compare commits

...

262 Commits

Author SHA1 Message Date
RiotRobot e15cf9976f v23.2.0 2023-01-31 10:46:15 +00:00
RiotRobot bdc3926417 Prepare changelog for v23.2.0 2023-01-31 10:46:12 +00:00
RiotRobot 1f58ee7f2c v23.2.0-rc.1 2023-01-24 11:21:25 +00:00
RiotRobot 2e28e9117a Prepare changelog for v23.2.0-rc.1 2023-01-24 11:21:22 +00:00
Andy Balaam a58a36e062 Tests for getRoomUpgradeHistory (#3088) 2023-01-24 10:45:04 +00:00
kegsay 6cf6a0c522 refactor: sliding sync: swap to lists-as-keys (#3086)
* refactor: sliding sync: swap to lists-as-keys

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

* Linting

* prettier

* add extra setListRanges test

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

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

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

* Linting

* prettier

* add extra setListRanges test

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

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

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

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

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

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

This test is just sticking a "redacted_because" property into a regular event,
which is a bit of a nonsense.
2023-01-17 11:27:30 +00:00
RiotRobot 4179f2978d v23.1.0-rc.4 2023-01-17 09:11:54 +00:00
RiotRobot 97df6db49c Prepare changelog for v23.1.0-rc.4 2023-01-17 09:11:52 +00:00
ElementRobot 3e693fab23 [Backport staging] Correctly handle limited sync responses by resetting the thread timeline (#3069)
Co-authored-by: Janne Mareike Koschinski <jannemk@element.io>
2023-01-17 09:06:28 +00:00
Janne Mareike Koschinski a34d06c7c2 Correctly handle limited sync responses by resetting the thread timeline (#3056)
* Reset thread livetimelines when desynced
* Implement workaround for https://github.com/matrix-org/synapse/issues/14830
2023-01-16 16:27:28 +00:00
David Baker 7b10fa367d Merge pull request #3066 from matrix-org/dbkr/olm_savesession_undecryptable_todevice_debug
Add some debugging & a debug event for decryption
2023-01-13 21:40:36 +00:00
Šimon Brandner 7f5d7091de Fix typos in src/webrtc/ (#3065) 2023-01-13 19:56:55 +01:00
David Baker 96f673ae92 Merge branch 'develop' into dbkr/olm_savesession_undecryptable_todevice_debug 2023-01-13 18:36:22 +00:00
David Baker 79faee7a67 Add emit so tests don't throw 2023-01-13 18:32:21 +00:00
David Baker 89d2984432 Add some debugging & a debug event for decryption
Adds a log line whenever we save a session and also adds an event
that's fired whenever we get a to-device event we can't decrypt
(hopefully the comment explains all).
2023-01-13 18:24:33 +00:00
Travis Ralston bc78784688 Extract v1 extensible events polls types out of the events-sdk (#3062)
* Extract v1 extensible events polls out of events-sdk

* Appease tsdoc?

* Appease naming standards

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

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

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

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

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-01-12 17:10:42 +00:00
Richard van der Hoff aa1e118f18 Fix failure to start in firefox private browser (#3058) 2023-01-12 17:03:16 +00:00
RiotRobot c10152e098 v23.1.0-rc.2 2023-01-12 13:31:20 +00:00
RiotRobot 6b7efbcd91 Prepare changelog for v23.1.0-rc.2 2023-01-12 13:31:18 +00:00
Richard van der Hoff d23c3cb8b2 Improve logging in legacy megolm code (#3043)
* Use a PrefixedLogger throughout `megolm.ts`

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

* Clean up logging in megolm.ts

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

* Log users that we find in the room when encrypting

* Reduce log verbosity on decryption retries
2023-01-12 11:49:32 +00:00
Richard van der Hoff 9e37980e2d Handle edits which are bundled with an event, per MSC3925 (#3045) 2023-01-12 10:53:36 +00:00
renovate[bot] de176dbd66 Update all non-major dependencies (#3053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-11 16:55:07 +00:00
renovate[bot] ac10b40f67 Update dependency @babel/core to v7.20.12 (#3054)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-11 16:46:47 +00:00
Michael Telatynski f35298a326 [Release] Fix browser entrypoint (#3052) 2023-01-11 16:31:02 +00:00
Michael Telatynski d7bf0f85c0 Fix browser entrypoint (#3051) 2023-01-11 15:17:55 +00:00
RiotRobot 1d87f5b163 v23.1.0-rc.1 2023-01-11 13:29:05 +00:00
RiotRobot 3e97067b3e Prepare changelog for v23.1.0-rc.1 2023-01-11 13:29:02 +00:00
Michael Telatynski 3ce582d004 Fix dated example (#3049) 2023-01-11 13:08:30 +00:00
Michael Telatynski 3e48c76a77 Avoid use of mkdirp (#3050) 2023-01-11 12:02:56 +00:00
Germain 8e29f8ead0 Improve hasUserReadEvent and getUserReadUpTo realibility with threads (#3031)
Co-authored-by: Patrick Cloke <clokep@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-01-11 09:53:27 +00:00
Travis Ralston 185ded4ebc Remove extensible events v1 field population on legacy events (#3040)
* Remove extensible events v1 field population on legacy events

With extensible events v2, affected events are now gated by a room version, so we don't need this code anymore. 

The proposal has generally moved away from mixing m.room.message with extensible fields as well.

* Run prettier

* Remove unstable identifier from tests too

* Run prettier again
2023-01-10 09:19:55 -07:00
Andy Balaam 3564a3546f Merge pull request #3039 from andybalaam/factor-out-findPredecessor
Factor out a (public) function to find a room's predecessor
2023-01-10 13:43:59 +00:00
Andy Balaam c6090325b3 Update id string to reflect spec 2023-01-10 13:29:06 +00:00
Andy Balaam 999e355136 Use Room.getLiveTimeline instead of deprecated this.currentState 2023-01-10 10:16:22 +00:00
Andy Balaam 7de4164444 Factor out a (public) function to find a room's predecessor 2023-01-10 10:16:22 +00:00
Andy Balaam e2ce379b56 Merge pull request #3038 from andybalaam/andybalaam/tests-for-getVisibleRooms
Tests for getVisibleRooms
2023-01-10 10:07:53 +00:00
Andy Balaam 424212cd65 Merge pull request #2915 from matrix-org/madlittlemods/stablize-msc3030-timestamp-to-event
Prefer stable `/timestamp_to_event` endpoint first - MSC3030
2023-01-09 17:01:10 +00:00
Andy Balaam c7c16256df Update comment to reflect commonality between 404 and 405 status 2023-01-09 16:53:52 +00:00
Andy Balaam 8a4c95ee72 Tests for getVisibleRooms 2023-01-09 12:17:53 +00:00
Clark Fischer c3d422f5fb Remove 'qs' dependency (#3033)
'qs' appears to be unused since 34c5598 (PR #2719).

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

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-06 22:37:05 +00:00
Šimon Brandner fdb80ad259 Remove video track when muting video (#3028) 2023-01-06 12:09:01 -05:00
Andy Balaam 981acf0044 Rename test to fit renamed function.
Co-authored-by: Eric Eastwood <erice@element.io>
2023-01-06 16:38:02 +00:00
Andy Balaam f00f70bfb8 Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event 2023-01-06 15:38:08 +00:00
Andy Balaam 12cc7be31c Test 400, 429 and 502 responses 2023-01-06 15:33:48 +00:00
Andy Balaam 628bcbf33a Fall back to the unstable endpoint if we receive a 405 status 2023-01-06 15:22:58 +00:00
Andy Balaam c4ca0b2e07 Refactor timestampToEvent tests 2023-01-06 15:13:44 +00:00
Andy Balaam d7442147b9 Rename convertQueryDictToMap 2023-01-06 14:21:35 +00:00
Andy Balaam b1566ee540 Switch to a Map for convertQueryDictToStringRecord 2023-01-06 14:20:24 +00:00
Andy Balaam ca98d9ff11 Tests for convertQueryDictToStringRecord 2023-01-06 14:20:24 +00:00
Andy Balaam bba4a35665 Merge pull request #3034 from matrix-org/johannes/poll-start-sent-receipt
Make poll start event type available (PSG-962)
2023-01-06 11:10:00 +00:00
Germain 896f6227a0 Fix threaded cache receipt when event holds multiple receipts (#3026) 2023-01-06 10:27:35 +00:00
Johannes Marbach 4d10cf3074 Make poll start event type available 2023-01-06 10:17:29 +01:00
Kerry bb23df9423 Add alt event type matching in Relations model (#3018)
* allow alt event types in relations model

* remove unneccesary checks on remove relation

* comment

* assert on event emitted
2023-01-05 20:00:12 +00:00
Richard van der Hoff d02559cf3c Make error handling in decryptionLoop more generic (#3024)
Not everything is a `DecryptionError`, and there's no real reason that we
should only do retries for `DecryptionError`s
2023-01-05 15:02:19 +00:00
Richard van der Hoff ec6272aa3d Fix outgoing messages for rust-crypto (#3025)
It turns out that MatrixClient uses a `FetchHttpApi` instance with
`opts.onlyData = true`, so it was returning the json-parsed response rather
than the raw response. Change the way we call `authedRequest` so that we get
the raw body back.
2023-01-05 15:00:37 +00:00
Michael Weimann 695b773f8b Fix false key requests after verifying new device (#3029) 2023-01-05 15:27:09 +01:00
Richard van der Hoff 030abe1563 Pass to-device messages into rust crypto-sdk (#3021)
We need a separate API, because `ClientEvent.ToDeviceEvent` is only emitted for
successfully decrypted to-device events
2023-01-05 09:54:56 +00:00
Richard van der Hoff 22f10f71b8 indirect decryption attempts via Client (#3023)
... to reduce the number of things referring to `client.crypto`
2023-01-05 09:54:44 +00:00
renovate[bot] 9ca3e7272e chore(deps): lock file maintenance (#3014)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-04 13:44:26 -07:00
Richard van der Hoff 64119ef915 Avoid logical assignment operator (#3022)
Apparently `??=` was only added to javascript in ES12, and our eleweb build
doesn't support it.

Fixes breakage introduced in #3019.
2023-01-04 14:17:55 +00:00
Richard van der Hoff 9ac7165e99 Handle outgoing requests from rust crypto SDK (#3019)
The rust matrix-sdk-crypto has an `outgoingRequests()` method which we need to poll, and make the requested requests.
2023-01-04 12:17:42 +00:00
Hubert Chathi 6168cedf32 Avoid triggering decryption errors when decrypting redacted events (#3004) 2023-01-03 11:06:54 -05:00
Richard van der Hoff 7c34deecb6 Pass CryptoBackend into SyncApi (#3010)
I need to start calling back into the new rust crypto implementation from the /sync loops, so I need to pass it into SyncApi. To reduce the coupling, I've defined a new interface specifying the methods which exist for that purpose. Currently it's only onSyncCompleted.
2023-01-03 15:37:51 +00:00
Ankit cef5507ab1 Include 'yarn install' . (#3011) 2023-01-03 14:07:37 +00:00
Richard van der Hoff 9b372d23ca Factor SyncApi options out of IStoredClientOptions (#3009)
There are a couple of callback interfaces which are currently stuffed into
`IStoredClientOpts` to make it easier to pass them into the `SyncApi`
constructor.

Before we add more fields to this, let's separate it out to a separate object.
2023-01-03 13:38:21 +00:00
renovate[bot] e9fef19c8f Update dependency @types/node to v18.11.18 (#2984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-03 13:28:02 +00:00
Andy Balaam 9ebc91aa5a Merge pull request #3017 from andybalaam/andybalaam/use-env-variable-for-ref_name
In release.yml use an env section to get ref_name
2023-01-03 13:10:05 +00:00
Andy Balaam fcb75d547e Remove unneeded variable in workflow 2023-01-03 12:25:13 +00:00
Andy Balaam 33c9af952e Reformat release.yml 2023-01-03 10:25:59 +00:00
Andy Balaam 3b66b28e71 In release.yml use an env section to get ref_name 2023-01-03 10:21:40 +00:00
Travis Ralston 7d37bb1edb Remove usage of v1 Identity Server API (#3003)
* Remove usage of v1 Identity Server API

It's been deprecated for over a year at this point - everyone should be able to support v2.

* Missed one.
2023-01-03 00:59:12 -07:00
Travis Ralston 0f717a9306 Update comment for m.key.verification.ready event type definition (#3006) 2023-01-03 00:58:55 -07:00
Richard van der Hoff ce776b9989 Break coupling between Room and Crypto.trackRoomDevices (#2998)
`Room` and `Crypto` currently have some tight coupling in the form of a call to
`trackRoomDevices` when out-of-band members are loaded. We can improve this by
instead having Crypto listen out for a `RoomSateEvent.Update` notification.
2022-12-23 11:03:14 +00:00
kegsay ff1b0e51ea Merge pull request #3008 from matrix-org/kegan/upload-otks
bugfix: upload OTKs in sliding sync mode
2022-12-22 16:26:47 +01:00
Kegan Dougal 21e66a5c34 bugfix: upload OTKs in sliding sync mode 2022-12-22 14:52:16 +00:00
Germain aead401005 Apply edits discovered from sync after thread is initialised (#3002) 2022-12-22 08:54:55 +00:00
Travis Ralston af9525ed5f Add device_id to /account/whoami types (#3005)
* Add `device_id` to `/account/whoami` types

https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3accountwhoami

* Appease the linter

* Modernize area of code

* Remove unused eslint disable comment
2022-12-21 16:46:10 -07:00
RiotRobot 1ebcac37cc Resetting package fields for development 2022-12-21 16:49:19 +00:00
RiotRobot 51a4cc5e18 Fix post-release script compatibility with prettier 2022-12-21 16:49:11 +00:00
RiotRobot 48baa6315c Merge branch 'master' into develop
# Conflicts:
#	release.sh
#	spec/integ/matrix-client-event-timeline.spec.ts
2022-12-21 16:48:43 +00:00
RiotRobot efc87d8084 v23.0.0 2022-12-21 16:40:30 +00:00
RiotRobot aab873fc58 Prepare changelog for v23.0.0 2022-12-21 16:40:28 +00:00
RiotRobot 52ed04c825 Fix release call to prettier 2022-12-21 16:38:37 +00:00
ElementRobot fdd1428e19 [Backport staging] Fix release scripts to not fight with prettier (#3000)
(cherry picked from commit ec2405ac99)

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-12-21 09:37:40 +00:00
Michael Telatynski ec2405ac99 Fix release scripts to not fight with prettier (#2999) 2022-12-21 09:31:16 +00:00
ElementRobot b31712f2ff [Backport staging] Threads are missing from the timeline (#2997)
(cherry picked from commit 4f86eee250)

Co-authored-by: Janne Mareike Koschinski <jannemk@element.io>
2022-12-20 17:27:18 +00:00
kegsay 61e2606bc4 Merge pull request #2991 from S7evinK/s7evink/slidingsync-unsub
sliding sync: Fix issue where no unsubs are sent when switching rooms
2022-12-20 17:57:19 +01:00
Richard van der Hoff 45f6c5b079 Add exportRoomKeys to CryptoBackend (#2970)
Element-web calls `exportRoomKeys` on logout, so we need a stub implementation
to get it EW working with the rust crypto sdk.
2022-12-20 11:11:00 +00:00
Michael Weimann b83c372848 Implement MSC3912: Relation-based redactions (#2954) 2022-12-20 09:22:26 +01:00
Šimon Brandner 6d58a54039 Don't use RoomMember as a calls a key on GroupCall (#2993) 2022-12-19 14:53:08 +01:00
Till 3872c5f099 Update src/sliding-sync.ts
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-12-19 11:54:20 +01:00
Janne Mareike Koschinski 4f86eee250 Threads are missing from the timeline (#2996) 2022-12-19 10:32:37 +00:00
David Baker 618242ef3c Merge pull request #2992 from matrix-org/dbkr/close_all_streams
Close all streams when a call ends
2022-12-19 10:31:36 +00:00
David Baker 96ee5b1256 Close all streams when a call ends
We didn't close streams in group calls (presumably from back when
we used the same stream for all calls rather than cloning?) but this
left stray screenshare streams in the mediahandler when a participant
left whilst we were screensharing.

Fixes https://github.com/vector-im/element-call/issues/742
2022-12-16 18:19:36 +00:00
Till Faelligen 8af0ff111b Add tests for custom subscriptions 2022-12-16 13:30:40 +01:00
Till Faelligen a04800f030 Fix issue where no unsubs are sent when switching rooms 2022-12-16 11:49:31 +01:00
Eric Eastwood 4683fbe848 Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event
Conflicts:
	spec/unit/matrix-client.spec.ts
2022-12-16 00:56:06 -06:00
renovate[bot] c973b26fa2 Update all non-major dependencies (#2981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-15 22:06:40 -07:00
renovate[bot] 7b96c730b8 Update typescript-eslint monorepo to v5.46.0 (#2985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-15 22:05:22 -07:00
renovate[bot] f8bf6083de Update dependency uuid to v9 (#2988)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-15 22:05:05 -07:00
Anderas 447319737a Merge pull request #2920 from Anderas/resume-todevice-queue
Resume to-device message queue after resumed sync
2022-12-14 14:27:47 +00:00
Janne Mareike Koschinski 39e127b4e3 Write test to validate #2971 (#2972) 2022-12-14 13:01:52 +01:00
Richard van der Hoff 15ef8fabb7 Introduce a mechanism for using the rust-crypto-sdk (#2969)
This PR introduces MatrixClient.initRustCrypto, which is similar to initCrypto, except that it will use the Rust crypto SDK instead of the old libolm-based implementation.

This is very much not something you want to use in production code right now, because the integration with the rust sdk is extremely skeletal and almost everything crypto-related will raise an exception rather than doing anything useful.

It is, however, enough to demonstrate the loading of the wasmified rust sdk in element web, and a react sdk with light modifications can successfully log in and out.

Part of vector-im/element-web#21972.
2022-12-14 11:02:02 +00:00
renovate[bot] df42014ef5 Update dependency @types/jest to v29.2.4 (#2980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-14 10:55:40 +00:00
Michael Weimann b765b18381 Add prettier formatting to .git-blame-ignore-revs (#2979) 2022-12-14 11:46:18 +01:00
RiotRobot 6cc8e4436c v23.0.0-rc.1 2022-12-14 08:49:15 +00:00
RiotRobot 7f1fe46c7c Prepare changelog for v23.0.0-rc.1 2022-12-14 08:49:15 +00:00
Kerry b2a10e6db3 Support MSC3391: Account data deletion (#2967)
* add deleteAccountData endpoint

* check server support and test

* test current state of memorystore

* interpret account data events with empty content as deleted

* add handling for (future) stable version of endpoint

* add getSafeUserId

* user getSafeUserId in deleteAccountData

* better jsdoc for throws documentation
2022-12-14 04:14:21 +00:00
Eric Eastwood a0aa5074ed Fix prefix lint
See https://github.com/matrix-org/matrix-js-sdk/pull/2915#discussion_r1043951639
2022-12-13 16:37:47 -06:00
Eric Eastwood 70a033c2fd Prettier fixes 2022-12-13 16:26:14 -06:00
Eric Eastwood fcf12b49e3 Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event
Conflicts:
	spec/unit/matrix-client.spec.ts
	src/client.ts
2022-12-13 16:18:33 -06:00
RiotRobot 284a475dfb Exclude CHANGELOG from prettier and undo what it did 2022-12-13 15:35:12 +00:00
Janne Mareike Koschinski 193c38523c Fix messages loaded during initial fetch ending up out of order (#2971)
* Fix messages loaded during initial fetch ending up out of order
2022-12-13 15:23:43 +01:00
Andy Uhnak 11ac3d9e58 Add unit tests 2022-12-13 12:26:52 +00:00
Andy Uhnak 071d5e71e4 Resume to-device message queue after resumed sync 2022-12-13 11:54:52 +00:00
Michael Telatynski acd220b8a9 Tweak how the SonarCloud scan fails (#2947) 2022-12-13 09:59:52 +00:00
Germain 74b30246d0 Avoid creating duplicate threads (#2968) 2022-12-13 09:54:44 +00:00
Richard van der Hoff 9c17eb6c14 Begin factoring out a CryptoBackend interface (#2955)
Part of https://github.com/vector-im/element-web/issues/21972. Eventually I want to replace the whole of the current `Crypto` implementation with an alternative implementation, but in order to get from here to there, I'm factoring out a common interface which will be implemented by both implementations.

I'm also determined to fix the problem where the innards of the crypto implementation are exposed to applications via the `MatrixClient.crypto` property.

It's not (yet) entirely clear what shape this interface should be, so I'm going with a minimal approach and adding things as we know we need them. This means that we need to keep the old `client.crypto` property around as well as a new `client.cryptoBackend` property. Eventually `client.crypto` will go away, but that will be a breaking change in the js-sdk.
2022-12-12 17:49:39 +00:00
Janne Mareike Koschinski 8293011ee2 Fix #23916: Prevent edits of the last message in a thread getting lost (#2951)
* Fix issue where the root event of a thread had to be loaded in a complicated way
* Fix issue where edits to the last event of a thread would get lost
* Fix issue where thread reply count would desync
* Refactor relations pagination mocking for tests
2022-12-12 18:22:16 +01:00
Richard van der Hoff 4c5f416b32 Factor out some utility functions in the megolm integration tests (#2958)
There's a lot of repetition here, which can be reduced with some utility functions.
2022-12-12 16:31:56 +00:00
Janne Mareike Koschinski 66c678e9fb Fix #23919: Root message for new thread loaded from network (#2965)
* Load root events of threads without additional network roundtrip
2022-12-12 16:30:48 +01:00
Richard van der Hoff 8a892ede23 Exclude generated files from prettier (#2961)
The example directories include symlinks to the generated matrix.js, which we
should not prettify.
2022-12-12 10:37:52 +00:00
Germain 2dd06e368e Fix infinite loop when restoring cached read receipts (#2963) 2022-12-09 15:20:50 +00:00
Damir Jelić fc501de081 Don't swallow up errors coming from the shareSession call (#2962)
A call to ensureSession() has two steps:
    1. prepareSession(), where an outbound group session might get created
       or rotated
    2. shareSession(), where an outbound group session might get
       encrypted and queued up to be sent to other devices

Both of those calls may mostly fail due to storage errors, yet only the
errors from prepareSession get propagated to the caller.

Errors from prepareSession will mean that you can't get an
outbound group session so you can't encrypt an event.

Errors from shareSession, especially if the error happens in the part
where the to-device requests are queued up to be sent out, mean that
other people will not be able to decrypt the events that will get
encrypted using the outbound group session.

Both of those cases are catastrophic, the second case is just much
harder to debug, since the error happens on another device at some
arbitrary point in the future.

Let's just return the error instead, people can then retry and the
storage issue might have been resolved, or at least the error becomes
visible when it happens.
2022-12-09 15:07:42 +00:00
Damir Jelić ada401f4c0 Make sure that MegolmEncryption.setupPromise always resolves (#2960)
ensureOutboundSession uses and modifies the setupPromise of the
MegolmEncryption class. Some comments suggest that setupPromise will
always resolve, in other words it should never contain a promise that
will get rejected.

Other comments also seem to suggest that the return value of
ensureOutboundSession, a promise as well, may fail.

The critical error here is that the promise that gets set as
the next setupPromise, as well as the promise that ensureOutboundSession
returns, is the same promise.

It seems that the intention was for setupPromise to contain a promise
that will always resolve to either `null` or `OutboundSessionInfo`.

We can see that a couple of lines before we set setupPromise to its new
value we construct a promise that logs and discards errors using the
`Promise.catch()` method.

The `Promise.catch()` method does not mutate the promise, instead it
returns a new promise. The intention of the original author might have
been to set the next setupPromise to the promise which `Promise.catch()`
produces.

This patch modifies the updating of setupPromise in the
ensureOutboundSession so that setupPromise discards errors correctly.

Using `>>=` to represent the promise chaining operation, setupPromise is
now updated using the following logic:

    setupPromise = previousSetupPromise >>= setup >>= discardErrors
2022-12-09 14:46:33 +00:00
Damir Jelić 41d762171e Apply prettier to the client.ts file (#2959) 2022-12-09 15:03:49 +01:00
Germain 5b6bebc1d7 Do not calculate highlight notifs for threads unknown to the room (#2957) 2022-12-09 12:41:51 +00:00
Andy Balaam 7b5e137ec0 Merge pull request #2906 from matrix-org/weeman1337/prettier
Add prettier
2022-12-09 12:16:16 +00:00
Michael Weimann 6e0901258c Switch to eslint-plugin-matrix-org 0.9 2022-12-09 10:51:15 +01:00
Michael Weimann 559fbdda26 Update eslint-plugin-matrix-org 2022-12-09 09:54:36 +01:00
Michael Weimann 72dac9a107 Apply manual code style fixes after prettier 2022-12-09 09:43:22 +01:00
Michael Weimann 349c2c2587 Apply prettier formatting 2022-12-09 09:38:20 +01:00
Michael Weimann 08a9073bd5 Add prettier 2022-12-09 09:34:01 +01:00
Eric Eastwood 9841f92415 Fix some eslint 2022-12-08 18:37:33 -06:00
Eric Eastwood ed91bd9c11 Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event
Conflicts:
	spec/unit/matrix-client.spec.ts
	src/client.ts
2022-12-08 18:29:14 -06:00
Eric Eastwood c953fc9fb7 Update casing
See https://github.com/matrix-org/matrix-js-sdk/pull/2915#discussion_r1041542066
2022-12-08 17:56:53 -06:00
Eric Eastwood bf78a64d82 Remove console coloring in favor of future PR
See https://github.com/matrix-org/matrix-js-sdk/pull/2915#discussion_r1041539703
2022-12-08 17:47:56 -06:00
Šimon Brandner ae849fdd46 Minor VoIP stack improvements (#2946)
* Add `IGroupCallRoomState`

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

* Export values into `const`s

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

* Add `should correctly emit LengthChanged`

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

* Add `ICE disconnected timeout`

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

* Improve typing

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

* Don't cast `getContent()`

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

* Use `Date.now()` for call length

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

* Type fix

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

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-12-08 19:51:05 +01:00
Richard van der Hoff 39cf212628 Expose a new 'userHasCrossSigningKeys' method (#2950) 2022-12-08 11:53:38 +00:00
Richard van der Hoff 224e592701 Fix examples/browser/browserTest.js (#2952)
This seems to have been broken for ages
2022-12-08 10:43:20 +00:00
Germain 16d791b038 Cache read receipts for unknown threads (#2953) 2022-12-08 09:54:10 +00:00
Michael Telatynski c4006d752a Improve tsdoc types (#2940)
* Install eslint-plugin-jsdoc

* Enable lint rule jsdoc/no-types

* Make tsdoc more valid, add required hyphens and s/return/returns/g

* Stash tsdoc work

* Fix mistypes

* Stash

* Stash

* More tsdoc work

* Remove useless doc params

* Fixup docs

* Apply suggestions from code review

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

* Update src/crypto/verification/request/ToDeviceChannel.ts

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

* Update src/client.ts

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

* Update src/client.ts

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

* Update src/client.ts

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

* Apply suggestions from code review

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

* Iterate

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2022-12-07 18:01:54 +00:00
Richard van der Hoff a9e7a46c56 Upload device keys during initCrypto (#2872)
Rather than waiting for the application to call `.startClient`, upload the
device keys during `initCrypto()`. Element-R is going to approach this slightly
differently (it wants to manage the decision on key uploads itself), so this
lays some groundwork by collecting the libolm-specific bits together.
2022-12-07 13:48:41 +00:00
Michael Telatynski 4a7365f32f Fix release documentation (#2949) 2022-12-07 13:06:41 +00:00
Germain a071a82a03 Update test runner instructions (#2948) 2022-12-07 11:18:32 +00:00
Michael Telatynski 8d018f9c2d Enable noImplicitAny (#2895)
* Stash noImplicitAny work

* Enable noImplicitAny

* Update olm

* Fun

* Fix msgid stuff

* Fix tests

* Attempt to fix Browserify
2022-12-06 18:21:44 +00:00
Richard van der Hoff 6f81371e61 Fix the message ID on key-share messages (#2945)
https://github.com/matrix-org/matrix-js-sdk/pull/2938 introduced message IDs on
outgoing to-device messages, but a typo meant that the IDs on key-share
messages were excessive.
2022-12-06 16:45:21 +00:00
RiotRobot ccab6985ad Resetting package fields for development 2022-12-06 12:34:38 +00:00
RiotRobot 569adc7b0c Merge branch 'master' into develop 2022-12-06 12:34:35 +00:00
RiotRobot b6369cc2bd v22.0.0 2022-12-06 12:32:58 +00:00
RiotRobot e6e079f487 Prepare changelog for v22.0.0 2022-12-06 12:32:58 +00:00
Richard van der Hoff 683e7fba4a Add a message ID on each to-device message (#2938)
To make it easier to track down where to-device messages are getting lost,
add a custom property to each one, and log its value. Synapse will also log
this property.
2022-12-06 10:31:48 +00:00
Šimon Brandner 2c8eece5ca Don't expose calls on GroupCall (#2941) 2022-12-05 18:44:13 +01:00
kegsay 4a4d493856 bugfix: sliding sync initial room timelines shouldn't notify (#2933)
* bugfix: sliding sync initial room timelines shouldn't notify

Flag timeline events as `fromCache` when `initial: true` rooms
are received. This stops notifications appearing inappropriately
when you scroll the room list or spider the room list, as it
causes `liveEvent=false`.

* Use num_live to detect liveness; with jest test

* Linting

* jsdoc

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-12-05 17:00:06 +00:00
Michael Weimann 11d8f562c5 Redo key sharing after own device verification (#2921) 2022-12-05 14:31:58 +01:00
Michael Telatynski 7799804762 Move @types deps into devDeps (#2927) 2022-12-02 16:27:59 +00:00
RiotRobot 83a1e07380 v22.0.0-rc.2 2022-12-02 16:23:53 +00:00
RiotRobot 7f3123ed65 Prepare changelog for v22.0.0-rc.2 2022-12-02 16:23:53 +00:00
ElementRobot 5a88a6c62a [Backport staging] Fix highlight notifications increasing when total notification is zero (#2939)
Co-authored-by: Germain <germains@element.io>
2022-12-02 16:16:41 +00:00
Janne Mareike Koschinski 8a7fd270e4 Move updated threads to the end of the thread list (#2923)
* Move updated threads to the end of the thread list
* Write new tests
2022-12-02 17:11:18 +01:00
ElementRobot 12cecbdcf1 [Backport staging] Fix synthesizeReceipt (#2934)
(cherry picked from commit 3577aa98b5)

Co-authored-by: Germain <germains@element.io>
2022-12-02 16:09:05 +00:00
Germain 53a45a34df Fix highlight notifications increasing when total notification is zero (#2937) 2022-12-02 15:41:15 +00:00
Janne Mareike Koschinski fa2eeac5b8 Improve perceived performance for threads (#2901)
* Improve perceived performance for threads
* Improve method naming and make it private
2022-12-02 15:50:19 +01:00
Janne Mareike Koschinski 720248466f Include pending events in thread summary and count again (#2922)
* Include pending events in thread summary and count again
* Pass through pending event status
2022-12-02 15:01:43 +01:00
Janne Mareike Koschinski 43bfa0c020 Switch to stable /relations endpoint, stop using unspecced original_event field (#2911)
* Switch to stable /relations endpoint, stop using unspecced original_event field
* Adapt the tests to the changed endpoint
2022-12-02 15:01:15 +01:00
Robin c17deb0806 Backport "Make GroupCall work better with widgets" to staging (#2936) 2022-12-02 10:34:41 +00:00
Robin 79ccd7c330 Merge pull request #2935 from robintown/entered-via-widget
Make GroupCall work better with widgets
2022-12-01 23:33:44 -05:00
Robin Townsend 9de9ff76b5 Test cleanMemberState 2022-12-01 23:27:31 -05:00
Robin Townsend c0090852ad Make GroupCall work better with widgets
If the client uses a widget to join group calls, like Element Web does, then the local device could be joined to the call without GroupCall knowing. This adds a field to GroupCall that allows the client to tell GroupCall when it's using another session to join the call.
2022-12-01 10:45:34 -05:00
Faye Duxovni 3870e3395d Add method to get outgoing room key requests for a given event (#2930)
* Add method to get outgoing room key requests for a given event

* Write test, fix typo

* Add test case for non-encrypted event
2022-12-01 09:49:36 +00:00
renovate[bot] a0f3e5d3bf Update babel monorepo (#2929)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 07:25:08 +00:00
renovate[bot] 720ea0e12e Update all non-major dependencies (#2928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 07:24:50 +00:00
Eric Eastwood 9a98e8008f Fix relevant strict ts error 2022-11-30 19:13:29 -06:00
Eric Eastwood ad8bb5d2cd Fix lints 2022-11-30 19:01:20 -06:00
Eric Eastwood 9a731cdf4f Add some comments 2022-11-30 18:53:23 -06:00
Eric Eastwood d1ede036e2 Add return type 2022-11-30 18:51:49 -06:00
Eric Eastwood d3f08fec03 Add tests 2022-11-30 18:45:05 -06:00
renovate[bot] d692a5dbe2 Update tspascoal/get-user-teams-membership action to v2 (#2925)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-30 14:22:05 +00:00
renovate[bot] 4362297edc Update dependency @typescript-eslint/eslint-plugin to v5.44.0 (#2924)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-30 14:21:43 +00:00
Richard van der Hoff 1606274c36 Process m.room.encryption events before emitting RoomMember events (#2914)
vector-im/element-web#23819 is an intermittent failure to correctly initiate a user verification process. The
root cause is as follows:

* In matrix-react-sdk, ensureDMExists tries to create an encrypted DM room, and assumes it is ready for use
  (including sending encrypted events) as soon as it receives a RoomStateEvent.NewMember notification
  indicating that the other user has been invited or joined. 

* However, in sync.ts, we process the membership events in a /sync response (including emitting
  RoomStateEvent.NewMember notifications), which is long before we process any m.room.encryption event.
    
* The upshot is that we can end up trying to send an encrypted event in the new room before processing
  the m.room.encryption event, which causes the crypto layer to blow up with an error of "Room was 
  previously configured to use encryption, but is no longer".

Strictly speaking, ensureDMExists probably ought to be listening for ClientEvent.Room as well as RoomStateEvent.NewMember; but that doesn't help us, because ClientEvent.Room is also emitted
before we process the crypto event.

So, we need to process the crypto event before we start emitting these other events; but a corollary of that 
is that we need to do so before we store the new room in the client's store. That makes things tricky, because
currently the crypto layer expects the room to have been stored in the client first.

So... we have to rearrange everything to pass the newly-created Room object into the crypto layer, rather than
just the room id, so that it doesn't need to rely on getting the Room from the client's store.
2022-11-30 10:53:38 +00:00
Michael Telatynski 8d6d262e5f Update CODEOWNERS (#2918) 2022-11-30 09:27:23 +00:00
Germain 3577aa98b5 Fix synthesizeReceipt (#2916) 2022-11-30 07:58:29 +00:00
renovate[bot] e0df53c2ed Update dependency terser to v5.16.0 (#2919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-30 01:11:32 +00:00
Marco Bartelt 6611cfa253 add-privileged-users-in-room (#2892) 2022-11-29 19:23:57 +00:00
Michael Weimann 25296bb486 Update @typescript-eslint/parser (#2917) 2022-11-29 18:38:38 +00:00
RiotRobot 31c4f6c16b v22.0.0-rc.1 2022-11-29 15:33:50 +00:00
RiotRobot 22271d22f8 Prepare changelog for v22.0.0-rc.1 2022-11-29 15:33:49 +00:00
Eric Eastwood 3a1897629a Prefer stable endpoint first 2022-11-28 22:50:37 -06:00
Robin 9d3ac66cf8 Merge pull request #2902 from robintown/group-call-participants
Refactor GroupCall participant management
2022-11-28 16:33:08 -05:00
Robin Townsend a4ad4ed2cf Merge branch 'develop' into group-call-participants 2022-11-28 16:11:24 -05:00
kegsay 7fd55a61bf Merge pull request #2912 from matrix-org/kegan/ss-receipts
sliding sync: add receipts extension
2022-11-28 18:22:11 +00:00
Kegan Dougal 847766c114 Review comments 2022-11-28 18:13:17 +00:00
Kegan Dougal c8c39052a7 Linting 2022-11-28 11:09:03 +00:00
Kegan Dougal 6592b2c205 sonarcloud 2022-11-28 10:58:38 +00:00
Michael Telatynski fc91153be4 Revert "Process m.room.encryption events before emitting RoomMember events" (#2913)
This reverts commit aaf3702c66.
2022-11-28 10:23:23 +00:00
Robin Townsend 5511a6ef8c Fix tests 2022-11-26 00:28:11 -05:00
Robin Townsend 19e02e894f Add a method for cleaning group call member state 2022-11-25 23:47:01 -05:00
Robin Townsend c54d61e158 Put creation timestamps on group calls 2022-11-25 23:45:45 -05:00
Robin Townsend 44da9040f4 Emit an event for outgoing group calls 2022-11-25 23:44:46 -05:00
Robin Townsend 995f5bf7d7 Merge branch 'develop' into group-call-participants 2022-11-25 11:56:45 -05:00
Travis Ralston ad16b26247 Define a spec support policy for the js-sdk (#2882)
* Define a spec support policy for the js-sdk

* Update timeline per team discussion
2022-11-25 09:07:09 -07:00
Richard van der Hoff aaf3702c66 Process m.room.encryption events before emitting RoomMember events (#2910)
* Update tests

* Call `Store.storeRoom` earlier

We're going to call `onCryptoEvent` earlier in `processSyncResponse`, but we
need to have stored the room before doing so. We therefore need to move the
call to `storeRoom` earlier.

We can actually reduce a bit of duplication by moving the call into
`SyncApi.createRoom`.

`storeRoom` has relatively few side-effects, so as far as I can tell this
should be pretty safe.

* Call onCryptoEvent before processing state events

This fixes the problematic race condition.
2022-11-25 13:47:28 +00:00
Kegan Dougal 74147b9943 Linting 2022-11-25 13:24:12 +00:00
Kegan Dougal 815370c5f9 sliding sync: add receipts extension 2022-11-25 13:21:16 +00:00
Damir Jelić a01d8e3174 Deprecate a function containing a typo (#2904) 2022-11-25 09:47:52 +00:00
Michael Telatynski 007b7dd242 Fix 3pid invite acceptance not working due to mxid being sent in body (#2907) 2022-11-25 09:22:10 +00:00
Florian Duros 77d6def1cc Add jest metrics (#2897)
* Add slow jest reporter
2022-11-24 13:06:19 +00:00
RiotRobot b318a77ece Resetting package fields for development 2022-11-22 11:25:53 +00:00
RiotRobot 1a90259326 Merge branch 'master' into develop 2022-11-22 11:25:49 +00:00
Robin Townsend f46ecf970c Refactor GroupCall participant management
This refactoring brings a number of improvements to GroupCall, which I've unfortunately had to combine into a single commit due to coupling:

- Moves the expiration timestamp field on call membership state to be per-device
- Makes the participants of a group call visible without having to enter the call yourself
- Enables users to join group calls from multiple devices
- Identifies active speakers by their call feed, rather than just their user ID
- Plays nicely with clients that can be in multiple calls in a room at once
- Fixes a memory leak caused by the call retry loop never stopping
- Changes GroupCall to update its state synchronously, and write back to room state asynchronously
  - This was already sort of halfway being done, but now we'd be committing to it
  - Generally improves the robustness of the state machine
  - It means that group call joins will appear instant, in a sense

For many reasons, this is a breaking change.
2022-11-21 12:16:44 -05:00
Richard van der Hoff dd98d7eb2c Further improvements to e2ee logging (#2900)
A followup to #2884. In particular, it is not always the case that the
`sender_key` in a `m.room_key_withheld` message is the sender of that message.
2022-11-21 14:33:30 +00:00
kegsay f3dc1c4ca2 Merge pull request #2893 from matrix-org/kegan/ss-typing
sliding sync: add support for typing extension
2022-11-21 11:45:14 +00:00
Michael Telatynski 305b83f8ea Merge branch 'develop' into kegan/ss-typing 2022-11-21 11:36:07 +00:00
Kegan Dougal acc488da64 typing events don't need to be in an array 2022-11-21 11:31:33 +00:00
renovate[bot] 7217f83db9 Update all (major) (#2890)
* Update all

* Pin p-retry

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-11-21 08:58:04 +00:00
renovate[bot] 37ea905faa Update all (#2899)
* Update all

* Update webrtc.ts

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-11-21 08:57:49 +00:00
Kegan Dougal 78de55b835 Review comments 2022-11-21 08:42:45 +00:00
David Baker cb410f463a Merge pull request #2898 from matrix-org/dbkr/dont_hang_up_calls_that_didnt_start
Don't hang up calls that haven't started yet
2022-11-18 17:33:31 +00:00
David Baker 72f9d5e6f9 Move check so we still do cleanup but just don't send the hangup 2022-11-18 17:08:50 +00:00
Michael Telatynski c389de98f3 Merge branch 'develop' into dbkr/dont_hang_up_calls_that_didnt_start 2022-11-18 16:39:16 +00:00
Michael Telatynski 20745dc9ac Add CI check with tsc --noImplicitAny (#2896) 2022-11-18 16:26:08 +00:00
David Baker 9410902049 Don't hang up calls that haven't started yet 2022-11-18 16:16:19 +00:00
Kegan Dougal a6badbb7fa s/room_ephemeral/typing/ 2022-11-18 14:30:30 +00:00
Kegan Dougal 0b65b199e3 Add ExtensionRoomEphemeral tests 2022-11-18 11:49:02 +00:00
Kegan Dougal c1138bc085 sliding sync ext: add room ephemeral events 2022-11-18 11:41:58 +00:00
Michael Telatynski c0f7df8c3b Update eslint-plugin-matrix-org and improve visibilities & types (#2887) 2022-11-18 09:20:53 +00:00
renovate[bot] e085609572 Update jest monorepo to v29.2.3 (#2888)
* Update jest monorepo to v29.2.3

* Trigger CI

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Weimann <michaelw@matrix.org>
2022-11-18 07:40:07 +00:00
renovate[bot] 0a4f86a79e Update typescript-eslint monorepo to v5.43.0 (#2889)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-18 08:29:30 +01:00
renovate[bot] 5d6ff6c7f9 Update dependency jest-environment-jsdom to v29 (#2891)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-17 18:20:34 -07:00
Richard van der Hoff ffcdfe166e Improve logging on Olm session errors (#2885)
I strongly suspect we are logging "secure channel corruption" errors when no
such thing happened, bit I can't quite figure it out yet. Add a bit more
logging to try to track them down.
2022-11-16 17:22:04 +00:00
Richard van der Hoff e1aa7d335b Improve logging of e2ee messages (#2884)
Attempt to make the way we log megolm session ids more consistent.
2022-11-16 15:45:44 +00:00
kegsay 29643e745c Merge pull request #2883 from matrix-org/kegan/custom-room-subs
Define sliding sync consts
2022-11-16 14:41:22 +00:00
Kegan Dougal 54622ce424 Define msc3575 consts 2022-11-16 13:22:31 +00:00
Germain ca2ae24d46 Read receipt accumulation for threads (#2881) 2022-11-16 10:58:42 +00:00
291 changed files with 34455 additions and 27311 deletions
+9 -6
View File
@@ -1,12 +1,15 @@
{
"sourceMaps": true,
"presets": [
["@babel/preset-env", {
"targets": {
"node": 10
},
"modules": "commonjs"
}],
[
"@babel/preset-env",
{
"targets": {
"node": 10
},
"modules": "commonjs"
}
],
"@babel/preset-typescript"
],
"plugins": [
+79 -53
View File
@@ -1,12 +1,6 @@
module.exports = {
plugins: [
"matrix-org",
"import",
],
extends: [
"plugin:matrix-org/babel",
"plugin:import/typescript",
],
plugins: ["matrix-org", "import", "jsdoc"],
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
env: {
browser: true,
node: true,
@@ -27,63 +21,95 @@ module.exports = {
"padded-blocks": ["error"],
"no-extend-native": ["error"],
"camelcase": ["error"],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always",
}],
"no-multi-spaces": ["error", { ignoreEOLComments: true }],
"space-before-function-paren": [
"error",
{
anonymous: "never",
named: "never",
asyncArrow: "always",
},
],
"arrow-parens": "off",
"prefer-promise-reject-errors": "off",
"quotes": "off",
"indent": "off",
"no-constant-condition": "off",
"no-async-promise-executor": "off",
// We use a `logger` intermediary module
"no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", {
name: "events",
message: "Please use TypedEventEmitter instead"
}],
"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",
}],
}],
"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: [
"**/*.ts",
],
extends: [
"plugin:matrix-org/typescript",
],
rules: {
// TypeScript has its own version of this
"@babel/no-invalid-this": "off",
overrides: [
{
files: ["**/*.ts"],
plugins: ["eslint-plugin-tsdoc"],
extends: ["plugin:matrix-org/typescript"],
rules: {
// TypeScript has its own version of this
"@babel/no-invalid-this": "off",
// We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off",
// 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"],
// 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",
"quotes": "off",
// We use a `logger` intermediary module
"no-console": "error",
},
},
}],
{
// We don't need amazing docs in our spec files
files: ["src/**/*.ts"],
rules: {
"tsdoc/syntax": "error",
// We use some select jsdoc rules as the tsdoc linter has only one rule
"jsdoc/no-types": "error",
"jsdoc/empty-tags": "error",
"jsdoc/check-property-names": "error",
"jsdoc/check-values": "error",
// These need a bit more work before we can enable
// "jsdoc/check-param-names": "error",
// "jsdoc/check-indentation": "error",
},
},
{
files: ["spec/**/*.ts"],
rules: {
// We don't need super strict typing in test utilities
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
},
},
],
};
+2 -1
View File
@@ -38,4 +38,5 @@ cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
# Switch to ESLint from JSHint (Google eslint rules as a base)
e057956ede9ad1a931ff8050c411aca7907e0394
# prettier
349c2c2587c2885bb69eda4aa078b5383724cf5e
+6 -4
View File
@@ -1,4 +1,6 @@
* @matrix-org/element-web
/src/webrtc @matrix-org/element-call-reviewers
/spec/*/webrtc @matrix-org/element-call-reviewers
* @matrix-org/element-web
/.github/workflows/** @matrix-org/element-web-app-team
/package.json @matrix-org/element-web-app-team
/yarn.lock @matrix-org/element-web-app-team
/src/webrtc @matrix-org/element-call-reviewers
/spec/*/webrtc @matrix-org/element-call-reviewers
+3 -3
View File
@@ -2,9 +2,9 @@
## Checklist
* [ ] 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))
- [ ] 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:
+2 -4
View File
@@ -1,6 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>matrix-org/renovate-config-element-web"
]
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>matrix-org/renovate-config-element-web"]
}
+26 -26
View File
@@ -1,30 +1,30 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
branches:
- develop
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 }}
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 }}
+28 -28
View File
@@ -1,34 +1,34 @@
name: Deploy documentation PR preview
on:
workflow_run:
workflows: [ "Static Analysis" ]
types:
- completed
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
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@bd10f381a96414ce2b13a11bfa89902ba7cea07f # v2.24.3
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
- 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
+22 -22
View File
@@ -1,27 +1,27 @@
name: Notify Downstream Projects
on:
push:
branches: [ develop ]
push:
branches: [develop]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
notify-downstream:
# Only respect triggers from our develop branch, ignore that of forks
if: github.repository == 'matrix-org/matrix-js-sdk'
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- repo: vector-im/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
notify-downstream:
# Only respect triggers from our develop branch, ignore that of forks
if: github.repository == 'matrix-org/matrix-js-sdk'
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- repo: vector-im/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
runs-on: ubuntu-latest
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
event-type: ${{ matrix.event }}
runs-on: ubuntu-latest
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
event-type: ${{ matrix.event }}
+82 -82
View File
@@ -1,91 +1,91 @@
name: Pull Request
on:
pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ]
workflow_call:
inputs:
labels:
type: string
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
required: false
description: "No longer used, uses allchange logic now, will be removed at a later date"
secrets:
ELEMENT_BOT_TOKEN:
required: true
pull_request_target:
types: [opened, edited, labeled, unlabeled, synchronize]
workflow_call:
inputs:
labels:
type: string
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
required: false
description: "No longer used, uses allchange logic now, will be removed at a later date"
secrets:
ELEMENT_BOT_TOKEN:
required: true
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
jobs:
changelog:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
requireLabel: true
changelog:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
requireLabel: true
prevent-blocked:
name: Prevent Blocked
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v6
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
core.setFailed("Preventing merge whilst PR is marked blocked!");
prevent-blocked:
name: Prevent Blocked
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v6
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
core.setFailed("Preventing merge whilst PR is marked blocked!");
community-prs:
name: Label Community PRs
runs-on: ubuntu-latest
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@v1
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
organization: matrix-org
team: Core Team
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
community-prs:
name: Label Community PRs
runs-on: ubuntu-latest
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@v2
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
organization: matrix-org
team: Core Team
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
uses: actions/github-script@v6
with:
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Z-Community-PR']
});
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
uses: actions/github-script@v6
with:
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Z-Community-PR']
});
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-latest
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
});
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-latest
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
});
github.rest.pulls.update({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
});
github.rest.pulls.update({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
});
+33 -33
View File
@@ -1,41 +1,41 @@
# Must only be called from `release#published` triggers
name: Publish to npm
on:
workflow_call:
secrets:
NPM_TOKEN:
required: true
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
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: 🔧 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: 🔨 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: 🚀 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 }}
- 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 }}
+46 -45
View File
@@ -1,58 +1,59 @@
name: Release Process
on:
release:
types: [ published ]
release:
types: [published]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
jsdoc:
name: Publish Documentation
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
jsdoc:
name: Publish Documentation
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
- name: 📖 Generate JSDoc
run: "yarn gendoc"
- name: 📖 Generate JSDoc
run: "yarn gendoc"
- name: 📋 Copy to temp
run: |
cp -a "./_docs" "$RUNNER_TEMP/"
- name: 📋 Copy to temp
run: |
cp -a "./_docs" "$RUNNER_TEMP/"
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v3
with:
ref: gh-pages
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v3
with:
ref: gh-pages
- name: 🔪 Prepare
run: |
tag="${{ github.ref_name }}"
VERSION="${tag#v}"
[ ! -e "$VERSION" ] || rm -r $VERSION
cp -r $RUNNER_TEMP/docs/ $VERSION
- name: 🔪 Prepare
env:
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
VERSION="${GITHUB_REF_NAME#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</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
# Add the new directory to the index if it isn't there already
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
- name: 🚀 Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
keep_files: true
publish_dir: .
- name: 🚀 Deploy
uses: peaceiris/actions-gh-pages@v3
with:
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 }}
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+46 -44
View File
@@ -1,50 +1,52 @@
# Must only be called from a workflow_run in the context of the upstream repo
name: SonarCloud
on:
workflow_call:
secrets:
SONAR_TOKEN:
required: true
inputs:
extra_args:
type: string
required: false
description: "Extra args to pass to SonarCloud"
workflow_call:
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 }}
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"
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' }}
version_cmd: 'cat package.json | jq -r .version'
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
coverage_run_id: ${{ github.event.workflow_run.id }}
coverage_workflow_name: tests.yml
coverage_extract_path: coverage
extra_args: ${{ inputs.extra_args }}
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.3
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
version_cmd: "cat package.json | jq -r .version"
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
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 }}
- 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 }}
+38 -38
View File
@@ -1,43 +1,43 @@
name: SonarQube
on:
workflow_run:
workflows: [ "Tests" ]
types:
- completed
workflow_run:
workflows: ["Tests"]
types:
- completed
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
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
# 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:
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 }}
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
+57 -57
View File
@@ -1,74 +1,74 @@
name: Static Analysis
on:
pull_request: { }
push:
branches: [ develop, master ]
pull_request: {}
push:
branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install"
- name: Install Deps
run: "yarn install"
- name: Typecheck
run: "yarn run lint:types"
- 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: 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"
- name: Typecheck (release mode)
run: "yarn run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install"
- name: Install Deps
run: "yarn install"
- name: Run Linter
run: "yarn run lint:js"
- name: Run Linter
run: "yarn run lint:js"
docs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
docs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install"
- name: Install Deps
run: "yarn install"
- 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
- name: Generate Docs
run: "yarn run gendoc"
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docs
path: _docs
# We'll only use this in a workflow_run, then we're done with it
retention-days: 1
+51 -42
View File
@@ -1,52 +1,61 @@
name: Tests
on:
pull_request: { }
push:
branches: [ develop, master ]
pull_request: {}
push:
branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
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@v3
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@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: ${{ matrix.node }}
- name: Setup Node
uses: actions/setup-node@v3
with:
cache: "yarn"
node-version: ${{ matrix.node }}
- name: Install dependencies
run: "yarn install"
- name: Install dependencies
run: "yarn install"
- name: Build
if: matrix.specs == 'browserify'
run: "yarn build"
- 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: 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 --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: Run tests with coverage and metrics
if: github.ref == 'refs/heads/develop'
run: |
yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/spec/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
coverage
!coverage/lcov-report
- name: Run tests with coverage
if: github.ref != 'refs/heads/develop'
run: |
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
coverage
!coverage/lcov-report
+32 -32
View File
@@ -1,38 +1,38 @@
name: Upgrade Dependencies
on:
workflow_dispatch: { }
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: true
workflow_dispatch: {}
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: true
jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Upgrade
run: yarn upgrade && yarn install
- name: Upgrade
run: yarn upgrade && yarn install
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/upgrade-deps
delete-branch: true
title: Upgrade dependencies
labels: |
Dependencies
T-Task
- name: Enable automerge
uses: peter-evans/enable-pull-request-automerge@v2
if: steps.cpr.outputs.pull-request-operation == 'created'
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/upgrade-deps
delete-branch: true
title: Upgrade dependencies
labels: |
Dependencies
T-Task
- name: Enable automerge
uses: peter-evans/enable-pull-request-automerge@v2
if: steps.cpr.outputs.pull-request-operation == 'created'
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
+26
View File
@@ -0,0 +1,26 @@
/_docs
.DS_Store
/.npmrc
/*.log
package-lock.json
.lock-wscript
build/Release
coverage
lib-cov
out
/dist
/lib
/examples/browser/lib
/examples/crypto-browser/lib
/examples/voip/lib
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
/matrix-js-sdk-*.tgz
.vscode
.vscode/
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
/CHANGELOG.md
+1
View File
@@ -0,0 +1 @@
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");
+107
View File
@@ -1,3 +1,110 @@
Changes in [23.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.2.0) (2023-01-31)
==================================================================================================
## ✨ Features
* Implement decryption via the rust sdk ([\#3074](https://github.com/matrix-org/matrix-js-sdk/pull/3074)).
* Handle edits which are bundled with an event, per MSC3925 ([\#3045](https://github.com/matrix-org/matrix-js-sdk/pull/3045)).
## 🐛 Bug Fixes
* Add null check for our own member event ([\#3082](https://github.com/matrix-org/matrix-js-sdk/pull/3082)).
* Handle group call getting initialised twice in quick succession ([\#3078](https://github.com/matrix-org/matrix-js-sdk/pull/3078)). Fixes vector-im/element-call#847.
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952. Contributed by @justjanne.
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
* Fix spurious "Decryption key withheld" messages ([\#3061](https://github.com/matrix-org/matrix-js-sdk/pull/3061)). Fixes vector-im/element-web#23803.
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
Changes in [23.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.1) (2023-01-20)
==================================================================================================
## 🐛 Bug Fixes
* Fix backwards compability for environment not support Array.prototype.at ([\#3080](https://github.com/matrix-org/matrix-js-sdk/pull/3080)).
Changes in [23.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0) (2023-01-18)
==================================================================================================
## 🦖 Deprecations
* Remove extensible events v1 field population on legacy events ([\#3040](https://github.com/matrix-org/matrix-js-sdk/pull/3040)).
## ✨ Features
* Improve hasUserReadEvent and getUserReadUpTo realibility with threads ([\#3031](https://github.com/matrix-org/matrix-js-sdk/pull/3031)). Fixes vector-im/element-web#24164.
* Remove video track when muting video ([\#3028](https://github.com/matrix-org/matrix-js-sdk/pull/3028)). Fixes vector-im/element-call#209.
* Make poll start event type available (PSG-962) ([\#3034](https://github.com/matrix-org/matrix-js-sdk/pull/3034)).
* Add alt event type matching in Relations model ([\#3018](https://github.com/matrix-org/matrix-js-sdk/pull/3018)).
* Remove usage of v1 Identity Server API ([\#3003](https://github.com/matrix-org/matrix-js-sdk/pull/3003)).
* Add `device_id` to `/account/whoami` types ([\#3005](https://github.com/matrix-org/matrix-js-sdk/pull/3005)).
* Implement MSC3912: Relation-based redactions ([\#2954](https://github.com/matrix-org/matrix-js-sdk/pull/2954)).
* Introduce a mechanism for using the rust-crypto-sdk ([\#2969](https://github.com/matrix-org/matrix-js-sdk/pull/2969)).
* Support MSC3391: Account data deletion ([\#2967](https://github.com/matrix-org/matrix-js-sdk/pull/2967)).
## 🐛 Bug Fixes
* Fix threaded cache receipt when event holds multiple receipts ([\#3026](https://github.com/matrix-org/matrix-js-sdk/pull/3026)).
* Fix false key requests after verifying new device ([\#3029](https://github.com/matrix-org/matrix-js-sdk/pull/3029)). Fixes vector-im/element-web#24167 and vector-im/element-web#23333.
* Avoid triggering decryption errors when decrypting redacted events ([\#3004](https://github.com/matrix-org/matrix-js-sdk/pull/3004)). Fixes vector-im/element-web#24084.
* bugfix: upload OTKs in sliding sync mode ([\#3008](https://github.com/matrix-org/matrix-js-sdk/pull/3008)).
* Apply edits discovered from sync after thread is initialised ([\#3002](https://github.com/matrix-org/matrix-js-sdk/pull/3002)). Fixes vector-im/element-web#23921.
* Sliding sync: Fix issue where no unsubs are sent when switching rooms ([\#2991](https://github.com/matrix-org/matrix-js-sdk/pull/2991)).
* Threads are missing from the timeline ([\#2996](https://github.com/matrix-org/matrix-js-sdk/pull/2996)). Fixes vector-im/element-web#24036.
* Close all streams when a call ends ([\#2992](https://github.com/matrix-org/matrix-js-sdk/pull/2992)). Fixes vector-im/element-call#742.
* Resume to-device message queue after resumed sync ([\#2920](https://github.com/matrix-org/matrix-js-sdk/pull/2920)). Fixes matrix-org/element-web-rageshakes#17170.
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952.
Changes in [23.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.0.0) (2022-12-21)
==================================================================================================
## 🚨 BREAKING CHANGES
* Process `m.room.encryption` events before emitting `RoomMember` events ([\#2914](https://github.com/matrix-org/matrix-js-sdk/pull/2914)). Fixes vector-im/element-web#23819.
* Don't expose `calls` on `GroupCall` ([\#2941](https://github.com/matrix-org/matrix-js-sdk/pull/2941)).
## ✨ Features
* Support MSC3391: Account data deletion ([\#2967](https://github.com/matrix-org/matrix-js-sdk/pull/2967)).
* Add a message ID on each to-device message ([\#2938](https://github.com/matrix-org/matrix-js-sdk/pull/2938)).
* Enable multiple users' power levels to be set at once ([\#2892](https://github.com/matrix-org/matrix-js-sdk/pull/2892)). Contributed by @GoodGuyMarco.
* Include pending events in thread summary and count again ([\#2922](https://github.com/matrix-org/matrix-js-sdk/pull/2922)). Fixes vector-im/element-web#23642.
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
* Add method to get outgoing room key requests for a given event ([\#2930](https://github.com/matrix-org/matrix-js-sdk/pull/2930)).
## 🐛 Bug Fixes
* Fix messages loaded during initial fetch ending up out of order ([\#2971](https://github.com/matrix-org/matrix-js-sdk/pull/2971)). Fixes vector-im/element-web#23972.
* Fix #23919: Root message for new thread loaded from network ([\#2965](https://github.com/matrix-org/matrix-js-sdk/pull/2965)). Fixes vector-im/element-web#23919.
* Fix #23916: Prevent edits of the last message in a thread getting lost ([\#2951](https://github.com/matrix-org/matrix-js-sdk/pull/2951)). Fixes vector-im/element-web#23916 and vector-im/element-web#23942.
* Fix infinite loop when restoring cached read receipts ([\#2963](https://github.com/matrix-org/matrix-js-sdk/pull/2963)). Fixes vector-im/element-web#23951.
* Don't swallow errors coming from the shareSession call ([\#2962](https://github.com/matrix-org/matrix-js-sdk/pull/2962)). Fixes vector-im/element-web#23792.
* Make sure that MegolmEncryption.setupPromise always resolves ([\#2960](https://github.com/matrix-org/matrix-js-sdk/pull/2960)).
* Do not calculate highlight notifs for threads unknown to the room ([\#2957](https://github.com/matrix-org/matrix-js-sdk/pull/2957)).
* Cache read receipts for unknown threads ([\#2953](https://github.com/matrix-org/matrix-js-sdk/pull/2953)).
* bugfix: sliding sync initial room timelines shouldn't notify ([\#2933](https://github.com/matrix-org/matrix-js-sdk/pull/2933)).
* Redo key sharing after own device verification ([\#2921](https://github.com/matrix-org/matrix-js-sdk/pull/2921)). Fixes vector-im/element-web#23333.
* Move updated threads to the end of the thread list ([\#2923](https://github.com/matrix-org/matrix-js-sdk/pull/2923)). Fixes vector-im/element-web#23876.
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
Changes in [22.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v22.0.0) (2022-12-06)
==================================================================================================
## 🚨 BREAKING CHANGES
* Enable users to join group calls from multiple devices ([\#2902](https://github.com/matrix-org/matrix-js-sdk/pull/2902)).
## 🦖 Deprecations
* Deprecate a function containing a typo ([\#2904](https://github.com/matrix-org/matrix-js-sdk/pull/2904)).
## ✨ Features
* sliding sync: add receipts extension ([\#2912](https://github.com/matrix-org/matrix-js-sdk/pull/2912)).
* Define a spec support policy for the js-sdk ([\#2882](https://github.com/matrix-org/matrix-js-sdk/pull/2882)).
* Further improvements to e2ee logging ([\#2900](https://github.com/matrix-org/matrix-js-sdk/pull/2900)).
* sliding sync: add support for typing extension ([\#2893](https://github.com/matrix-org/matrix-js-sdk/pull/2893)).
* Improve logging on Olm session errors ([\#2885](https://github.com/matrix-org/matrix-js-sdk/pull/2885)).
* Improve logging of e2ee messages ([\#2884](https://github.com/matrix-org/matrix-js-sdk/pull/2884)).
## 🐛 Bug Fixes
* Fix 3pid invite acceptance not working due to mxid being sent in body ([\#2907](https://github.com/matrix-org/matrix-js-sdk/pull/2907)). Fixes vector-im/element-web#23823.
* Don't hang up calls that haven't started yet ([\#2898](https://github.com/matrix-org/matrix-js-sdk/pull/2898)).
* Read receipt accumulation for threads ([\#2881](https://github.com/matrix-org/matrix-js-sdk/pull/2881)).
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22)
==================================================================================================
+1 -3
View File
@@ -1,5 +1,3 @@
Contributing code to matrix-js-sdk
==================================
# Contributing code to matrix-js-sdk
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
+153 -149
View File
@@ -6,21 +6,25 @@
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
Matrix Javascript SDK
=====================
# Matrix JavaScript SDK
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
JavaScript. This SDK can be run in a browser or in Node.js.
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
browser or in Node.js.
Quickstart
==========
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports
is removed in v1.4 then the feature is _eligible_ for removal from the SDK when v1.8 is released. This SDK has no
guarantee on implementing all features of any particular spec release, currently. This can mean that the SDK will call
endpoints from before Matrix 1.1, for example.
# Quickstart
## In a browser
In a browser
------------
Download the browser version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
``<script>`` to your page. There will be a global variable ``matrixcs``
attached to ``window`` through which you can access the SDK. See below for how to
`<script>` to your page. There will be a global variable `matrixcs`
attached to `window` through which you can access the SDK. See below for how to
include libolm to enable end-to-end-encryption.
The browser bundle supports recent versions of browsers. Typically this is ES2015
@@ -29,8 +33,7 @@ or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
Please check [the working browser example](examples/browser) for more information.
In Node.js
----------
## In Node.js
Ensure you have the latest LTS version of Node.js installed.
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
@@ -39,14 +42,14 @@ If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn`
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.
``yarn add matrix-js-sdk``
`yarn add matrix-js-sdk`
```javascript
import * as sdk from "matrix-js-sdk";
const client = sdk.createClient("https://matrix.org");
client.publicRooms(function(err, data) {
import * as sdk from "matrix-js-sdk";
const client = sdk.createClient({ baseUrl: "https://matrix.org" });
client.publicRooms(function (err, data) {
console.log("Public Rooms: %s", JSON.stringify(data));
});
});
```
See below for how to include libolm to enable end-to-end-encryption. Please check
@@ -55,14 +58,14 @@ See below for how to include libolm to enable end-to-end-encryption. Please chec
To start the client:
```javascript
await client.startClient({initialSyncLimit: 10});
await client.startClient({ initialSyncLimit: 10 });
```
You can perform a call to `/sync` to get the current state of the client:
```javascript
client.once('sync', function(state, prevState, res) {
if(state === 'PREPARED') {
client.once("sync", function (state, prevState, res) {
if (state === "PREPARED") {
console.log("prepared");
} else {
console.log(state);
@@ -75,8 +78,8 @@ To send a message:
```javascript
const content = {
"body": "message text",
"msgtype": "m.text"
body: "message text",
msgtype: "m.text",
};
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
console.log(err);
@@ -86,11 +89,11 @@ client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
To listen for message events:
```javascript
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
if (event.getType() !== "m.room.message") {
return; // only use messages
}
console.log(event.event.content.body);
client.on("Room.timeline", function (event, room, toStartOfTimeline) {
if (event.getType() !== "m.room.message") {
return; // only use messages
}
console.log(event.event.content.body);
});
```
@@ -98,73 +101,70 @@ By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as
```javascript
Object.keys(client.store.rooms).forEach((roomId) => {
client.getRoom(roomId).timeline.forEach(t => {
console.log(t.event);
});
client.getRoom(roomId).timeline.forEach((t) => {
console.log(t.event);
});
});
```
What does this SDK do?
----------------------
## What does this SDK do?
This SDK provides a full object model around the Matrix Client-Server API and emits
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
- Handles syncing (via `/initialSync` and `/events`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
- Handles syncing (via `/initialSync` and `/events`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
Later versions of the SDK will:
- Expose a `RoomSummary` which would be suitable for a recents page.
- Provide different pluggable storage layers (e.g. local storage, database-backed)
Usage
=====
- Expose a `RoomSummary` which would be suitable for a recents page.
- Provide different pluggable storage layers (e.g. local storage, database-backed)
# Usage
Conventions
-----------
## Conventions
### Emitted events
The SDK will emit events using an ``EventEmitter``. It also
emits object models (e.g. ``Rooms``, ``RoomMembers``) when they
The SDK will emit events using an `EventEmitter`. It also
emits object models (e.g. `Rooms`, `RoomMembers`) when they
are updated.
```javascript
// Listen for low-level MatrixEvents
client.on("event", function(event) {
// Listen for low-level MatrixEvents
client.on("event", function (event) {
console.log(event.getType());
});
});
// Listen for typing changes
client.on("RoomMember.typing", function(event, member) {
// Listen for typing changes
client.on("RoomMember.typing", function (event, member) {
if (member.typing) {
console.log(member.name + " is typing...");
console.log(member.name + " is typing...");
} else {
console.log(member.name + " stopped typing.");
}
else {
console.log(member.name + " stopped typing.");
}
});
});
// start the client to setup the connection to the server
client.startClient();
// start the client to setup the connection to the server
client.startClient();
```
### Promises and Callbacks
@@ -181,11 +181,11 @@ The typical usage is something like:
});
```
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
Alternatively, if you have a Node.js-style `callback(err, result)` function,
you can pass the result of the promise into it with something like:
```javascript
matrixClient.someMethod(arg1, arg2).nodeify(callback);
matrixClient.someMethod(arg1, arg2).nodeify(callback);
```
The main thing to note is that it is problematic to discard the result of a
@@ -193,61 +193,65 @@ promise-returning function, as that will cause exceptions to go unobserved.
Methods which return a promise show this in their documentation.
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
via an optional ``callback`` argument. The callback support is now deprecated:
new methods do not include a ``callback`` argument, and in the future it may be
Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises,
via an optional `callback` argument. The callback support is now deprecated:
new methods do not include a `callback` argument, and in the future it may be
removed from existing methods.
Examples
--------
## Examples
This section provides some useful code snippets which demonstrate the
core functionality of the SDK. These examples assume the SDK is setup like this:
```javascript
import * as sdk from "matrix-js-sdk";
const myUserId = "@example:localhost";
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId
});
import * as sdk from "matrix-js-sdk";
const myUserId = "@example:localhost";
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId,
});
```
### Automatically join rooms when invited
```javascript
matrixClient.on("RoomMember.membership", function(event, member) {
if (member.membership === "invite" && member.userId === myUserId) {
matrixClient.joinRoom(member.roomId).then(function() {
console.log("Auto-joined %s", member.roomId);
});
}
});
matrixClient.on("RoomMember.membership", function (event, member) {
if (member.membership === "invite" && member.userId === myUserId) {
matrixClient.joinRoom(member.roomId).then(function () {
console.log("Auto-joined %s", member.roomId);
});
}
});
matrixClient.startClient();
matrixClient.startClient();
```
### Print out messages for all rooms
```javascript
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
if (event.getType() !== "m.room.message") {
return; // only print messages
}
console.log(
// the room name will update with m.room.name events automatically
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
);
});
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
if (event.getType() !== "m.room.message") {
return; // only print messages
}
console.log(
// the room name will update with m.room.name events automatically
"(%s) %s :: %s",
room.name,
event.getSender(),
event.getContent().body,
);
});
matrixClient.startClient();
matrixClient.startClient();
```
Output:
```
(My Room) @megan:localhost :: Hello world
(My Room) @megan:localhost :: how are you?
@@ -259,27 +263,24 @@ Output:
### Print out membership lists whenever they are changed
```javascript
matrixClient.on("RoomState.members", function(event, state, member) {
const room = matrixClient.getRoom(state.roomId);
if (!room) {
return;
}
const memberList = state.getMembers();
console.log(room.name);
console.log(Array(room.name.length + 1).join("=")); // underline
for (var i = 0; i < memberList.length; i++) {
console.log(
"(%s) %s",
memberList[i].membership,
memberList[i].name
);
}
});
matrixClient.on("RoomState.members", function (event, state, member) {
const room = matrixClient.getRoom(state.roomId);
if (!room) {
return;
}
const memberList = state.getMembers();
console.log(room.name);
console.log(Array(room.name.length + 1).join("=")); // underline
for (var i = 0; i < memberList.length; i++) {
console.log("(%s) %s", memberList[i].membership, memberList[i].name);
}
});
matrixClient.startClient();
matrixClient.startClient();
```
Output:
```
My Room
=======
@@ -289,8 +290,7 @@ Output:
(invite) @charlie:localhost
```
API Reference
=============
# API Reference
A hosted reference can be found at
http://matrix-org.github.io/matrix-js-sdk/index.html
@@ -304,21 +304,20 @@ host the API reference from the source files like this:
$ python -m http.server 8005
```
Then visit ``http://localhost:8005`` to see the API docs.
Then visit `http://localhost:8005` to see the API docs.
End-to-end encryption support
=============================
# End-to-end encryption support
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
application to make libolm available, via the ``Olm`` global.
application to make libolm available, via the `Olm` global.
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
It is also necessary to call `await matrixClient.initCrypto()` after creating a new
`MatrixClient` (but **before** calling `matrixClient.startClient()`) to
initialise the crypto layer.
If the ``Olm`` global is not available, the SDK will show a warning, as shown
below; ``initCrypto()`` will also fail.
If the `Olm` global is not available, the SDK will show a warning, as shown
below; `initCrypto()` will also fail.
```
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
@@ -330,46 +329,51 @@ specification.
To provide the Olm library in a browser application:
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
- download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
- load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
To provide the Olm library in a node.js application:
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
(replace the URL with the latest version you want to use from
- `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`
(replace the URL with the latest version you want to use from
https://packages.matrix.org/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
- `global.Olm = require('olm');` _before_ loading `matrix-js-sdk`.
If you want to package Olm as dependency for your node.js application, you can
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``. If your
application also works without e2e crypto enabled, add ``--optional`` to mark it
use `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`. If your
application also works without e2e crypto enabled, add `--optional` to mark it
as an optional dependency.
# Contributing
Contributing
============
*This section is for people who want to modify the SDK. If you just
want to use this SDK, skip this section.*
_This section is for people who want to modify the SDK. If you just
want to use this SDK, skip this section._
First, you need to pull in the right build tools:
```
$ yarn install
```
Building
--------
## Building
To build a browser version from scratch when developing::
```
$ yarn build
```
To run tests (Jasmine)::
To run tests (Jest):
```
$ yarn test
```
> **Note**
> The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass
To run linting:
```
$ yarn lint
```
+27 -27
View File
@@ -20,19 +20,19 @@ blurrier.
When we are low on disk space overall or near the group limit / origin quota:
* Chrome
* Log database may fail to start with AbortError
* IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
* When near the quota, QuotaExceededError is used more consistently
* Firefox
* The first error will be QuotaExceededError
* Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
* Once you start getting errors, the DB is effectively wedged in read-only
mode
* Can revive access if you reopen the DB
- Chrome
- Log database may fail to start with AbortError
- IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
- When near the quota, QuotaExceededError is used more consistently
- Firefox
- The first error will be QuotaExceededError
- Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
- Once you start getting errors, the DB is effectively wedged in read-only
mode
- Can revive access if you reopen the DB
## Cache Eviction
@@ -41,9 +41,9 @@ limited by a single quota, in practice, browsers appear to handle `localStorage`
separately from the others, so it has a separate quota limit and isn't evicted
when low on space.
* Chrome, Firefox
* IndexedDB for origin deleted
* Local Storage remains in place
- Chrome, Firefox
- IndexedDB for origin deleted
- Local Storage remains in place
## Persistent Storage
@@ -51,20 +51,20 @@ Storage Standard offers a `navigator.storage.persist` API that can be used to
request persistent storage that won't be deleted by the browser because of low
space.
* Chrome
* Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
* Firefox
* Firefox 67 shows a prompt to grant
* Reverting persistent seems to require revoking permission _and_ clearing
site data
- Chrome
- Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
- Firefox
- Firefox 67 shows a prompt to grant
- Reverting persistent seems to require revoking permission _and_ clearing
site data
## Storage Estimation
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
how much space remains.
* Chrome, Firefox
* Can run this at any time to request an estimate of space remaining
* Firefox
* Returns `0` for `usage` if a site is persisted
- Chrome, Firefox
- Can run this at any time to request an estimate of space remaining
- Firefox
- Returns `0` for `usage` if a site is persisted
+4 -3
View File
@@ -1,9 +1,10 @@
To try it out, **you must build the SDK first** and then host this folder:
```
$ npm run build
$ yarn install
$ yarn build
$ cd examples/browser
$ python -m SimpleHTTPServer 8003
$ python -m http.server 8003
```
Then visit ``http://localhost:8003``.
Then visit `http://localhost:8003`.
+2 -6
View File
@@ -1,11 +1,7 @@
console.log("Loading browser sdk");
var client = matrixcs.createClient("https://matrix.org");
client.publicRooms(function (err, data) {
if (err) {
console.error("err %s", JSON.stringify(err));
return;
}
var client = matrixcs.createClient({ baseUrl: "https://matrix.org" });
client.publicRooms().then(function (data) {
console.log("data %s [...]", JSON.stringify(data).substring(0, 100));
console.log("Congratulations! The SDK is working on the browser!");
var result = document.getElementById("result");
+15 -16
View File
@@ -1,18 +1,17 @@
<html lang="en">
<head>
<title>Test</title>
<meta charset="utf-8"/>
<link rel="icon" href="data:,">
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
Sanity Testing (check the console) : This example is here to make sure that
the SDK works inside a browser. It simply does a GET /publicRooms on
matrix.org
<br/>
You should see a message confirming that the SDK works below:
<br/>
<div id="result"></div>
</body>
<head>
<title>Test</title>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
Sanity Testing (check the console) : This example is here to make sure that the SDK works inside a browser. It
simply does a GET /publicRooms on matrix.org
<br />
You should see a message confirming that the SDK works below:
<br />
<div id="result"></div>
</body>
</html>
@@ -1,59 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Test Crypto in Browser</title>
<script src="lib/olm.js"></script>
<script src="lib/matrix.js"></script>
</head>
<body>
<h1>Testing export/import of Olm devices in the browser</h1>
<ul>
<li>
Make sure you built the current version of the Matrix JS SDK
(<code>yarn build</code>)
</li>
<li>
copy <code>olm.js</code> and <code>olm.wasm</code>
from a recent release of Olm (was tested with version 3.1.4)
in directory <code>lib/</code>
</li>
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
<li>
in the JS console, do:
<pre>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Test Crypto in Browser</title>
<script src="lib/olm.js"></script>
<script src="lib/matrix.js"></script>
</head>
<body>
<h1>Testing export/import of Olm devices in the browser</h1>
<ul>
<li>Make sure you built the current version of the Matrix JS SDK (<code>yarn build</code>)</li>
<li>
copy <code>olm.js</code> and <code>olm.wasm</code> from a recent release of Olm (was tested with version
3.1.4) in directory <code>lib/</code>
</li>
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
<li>
in the JS console, do:
<pre>
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
await aliceMatrixClient.exportDevice();
await aliceMatrixClient.getAccessToken();
</pre>
</li>
<li>
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page)
</li>
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
<li>
Do the following, replacing <code>ALICE_ID</code>
with the user ID of Alice (you can find it in the exported data)
<pre>
</pre
>
</li>
<li>
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere (<strong
>not</strong
>
in a JS variable as it will be destroyed when you refresh the page)
</li>
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
<li>
Do the following, replacing <code>ALICE_ID</code>
with the user ID of Alice (you can find it in the exported data)
<pre>
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
</pre>
</li>
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
<li>
Now do the following, using the exported data and the access token you saved previously:
<pre>
</pre
>
</li>
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
<li>
Now do the following, using the exported data and the access token you saved previously:
<pre>
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
</pre>
</li>
<li>You should see the message sent by Bob printed in the console.</li>
</ul>
</pre
>
</li>
<li>You should see the message sent by Bob printed in the console.</li>
</ul>
<script src="olm-device-export-import.js"></script>
</body>
</html>
<script src="olm-device-export-import.js"></script>
</body>
</html>
@@ -1,34 +1,26 @@
if (!Olm) {
console.error(
"global.Olm does not seem to be present."
+ " Did you forget to add olm in the lib/ directory?"
);
console.error("global.Olm does not seem to be present." + " Did you forget to add olm in the lib/ directory?");
}
const BASE_URL = 'http://localhost:8008';
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' };
const PASSWORD = 'password';
const BASE_URL = "http://localhost:8008";
const ROOM_CRYPTO_CONFIG = { algorithm: "m.megolm.v1.aes-sha2" };
const PASSWORD = "password";
// useful to create new usernames
window.randomHex = () => Math.floor(Math.random() * (10**6)).toString(16);
window.randomHex = () => Math.floor(Math.random() * 10 ** 6).toString(16);
window.newMatrixClient = async function (username) {
const registrationClient = matrixcs.createClient(BASE_URL);
const userRegisterResult = await registrationClient.register(
username,
PASSWORD,
null,
{ type: 'm.login.dummy' }
);
const userRegisterResult = await registrationClient.register(username, PASSWORD, null, { type: "m.login.dummy" });
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
userId: userRegisterResult.user_id,
accessToken: userRegisterResult.access_token,
deviceId: userRegisterResult.device_id,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
baseUrl: BASE_URL,
userId: userRegisterResult.user_id,
accessToken: userRegisterResult.access_token,
deviceId: userRegisterResult.device_id,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
@@ -36,15 +28,15 @@ window.newMatrixClient = async function (username) {
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
}
};
window.importMatrixClient = async function (exportedDevice, accessToken) {
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
deviceToImport: exportedDevice,
accessToken,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
baseUrl: BASE_URL,
deviceToImport: exportedDevice,
accessToken,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
@@ -52,71 +44,62 @@ window.importMatrixClient = async function (exportedDevice, accessToken) {
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
}
};
function extendMatrixClient(matrixClient) {
// automatic join
matrixClient.on('RoomMember.membership', async (event, member) => {
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) {
matrixClient.on("RoomMember.membership", async (event, member) => {
if (member.membership === "invite" && member.userId === matrixClient.getUserId()) {
await matrixClient.joinRoom(member.roomId);
// setting up of room encryption seems to be triggered automatically
// but if we don't wait for it the first messages we send are unencrypted
await matrixClient.setRoomEncryption(member.roomId, { algorithm: 'm.megolm.v1.aes-sha2' })
await matrixClient.setRoomEncryption(member.roomId, { algorithm: "m.megolm.v1.aes-sha2" });
}
});
matrixClient.onDecryptedMessage = message => {
console.log('Got encrypted message: ', message);
}
matrixClient.onDecryptedMessage = (message) => {
console.log("Got encrypted message: ", message);
};
matrixClient.on('Event.decrypted', (event) => {
if (event.getType() === 'm.room.message'){
matrixClient.on("Event.decrypted", (event) => {
if (event.getType() === "m.room.message") {
matrixClient.onDecryptedMessage(event.getContent().body);
} else {
console.log('decrypted an event of type', event.getType());
console.log("decrypted an event of type", event.getType());
console.log(event);
}
});
matrixClient.createEncryptedRoom = async function(usersToInvite) {
const {
room_id: roomId,
} = await this.createRoom({
visibility: 'private',
invite: usersToInvite,
matrixClient.createEncryptedRoom = async function (usersToInvite) {
const { room_id: roomId } = await this.createRoom({
visibility: "private",
invite: usersToInvite,
});
// matrixClient.setRoomEncryption() only updates local state
// but does not send anything to the server
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
// so we do it ourselves with 'sendStateEvent'
await this.sendStateEvent(
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG,
);
await this.setRoomEncryption(
roomId, ROOM_CRYPTO_CONFIG,
);
await this.sendStateEvent(roomId, "m.room.encryption", ROOM_CRYPTO_CONFIG);
await this.setRoomEncryption(roomId, ROOM_CRYPTO_CONFIG);
// Marking all devices as verified
let room = this.getRoom(roomId);
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
let members = (await room.getEncryptionTargetMembers()).map((x) => x["userId"]);
let memberkeys = await this.downloadKeys(members);
for (const userId in memberkeys) {
for (const deviceId in memberkeys[userId]) {
await this.setDeviceVerified(userId, deviceId);
}
for (const deviceId in memberkeys[userId]) {
await this.setDeviceVerified(userId, deviceId);
}
}
return roomId;
}
};
matrixClient.sendTextMessage = async function(message, roomId) {
return matrixClient.sendMessage(
roomId,
{
body: message,
msgtype: 'm.text',
}
)
}
}
matrixClient.sendTextMessage = async function (message, roomId) {
return matrixClient.sendMessage(roomId, {
body: message,
msgtype: "m.text",
});
};
}
+1 -2
View File
@@ -1,6 +1,5 @@
This is a functional terminal app which allows you to see the room list for a user, join rooms, send messages and view room membership lists.
To try it out, you will need to edit `app.js` to configure it for your `homeserver`, `access_token` and `user_id`. Then run:
```
@@ -24,7 +23,7 @@ Room list index commands:
Room commands:
'/exit' Return to the room list index.
'/members' Show the room member list.
$ /enter 2
[2015-06-12 15:14:54] Megan2 <<< herro
+140 -152
View File
@@ -5,7 +5,7 @@ var clc = require("cli-color");
var matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId
userId: myUserId,
});
// Data structures
@@ -14,15 +14,15 @@ var viewingRoom = null;
var numMessagesToShow = 20;
// Reading from stdin
var CLEAR_CONSOLE = '\x1B[2J';
var CLEAR_CONSOLE = "\x1B[2J";
var readline = require("readline");
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: completer
completer: completer,
});
rl.setPrompt("$ ");
rl.on('line', function(line) {
rl.on("line", function (line) {
if (line.trim().length === 0) {
rl.prompt();
return;
@@ -37,14 +37,11 @@ rl.on('line', function(line) {
if (line === "/exit") {
viewingRoom = null;
printRoomList();
}
else if (line === "/members") {
} else if (line === "/members") {
printMemberList(viewingRoom);
}
else if (line === "/roominfo") {
} else if (line === "/roominfo") {
printRoomInfo(viewingRoom);
}
else if (line === "/resend") {
} else if (line === "/resend") {
// get the oldest not sent event.
var notSentEvent;
for (var i = 0; i < viewingRoom.timeline.length; i++) {
@@ -54,76 +51,84 @@ rl.on('line', function(line) {
}
}
if (notSentEvent) {
matrixClient.resendEvent(notSentEvent, viewingRoom).then(function() {
printMessages();
rl.prompt();
}, function(err) {
printMessages();
print("/resend Error: %s", err);
rl.prompt();
});
matrixClient.resendEvent(notSentEvent, viewingRoom).then(
function () {
printMessages();
rl.prompt();
},
function (err) {
printMessages();
print("/resend Error: %s", err);
rl.prompt();
},
);
printMessages();
rl.prompt();
}
}
else if (line.indexOf("/more ") === 0) {
} else if (line.indexOf("/more ") === 0) {
var amount = parseInt(line.split(" ")[1]) || 20;
matrixClient.scrollback(viewingRoom, amount).then(function(room) {
printMessages();
rl.prompt();
}, function(err) {
print("/more Error: %s", err);
});
}
else if (line.indexOf("/invite ") === 0) {
matrixClient.scrollback(viewingRoom, amount).then(
function (room) {
printMessages();
rl.prompt();
},
function (err) {
print("/more Error: %s", err);
},
);
} else if (line.indexOf("/invite ") === 0) {
var userId = line.split(" ")[1].trim();
matrixClient.invite(viewingRoom.roomId, userId).then(function() {
printMessages();
rl.prompt();
}, function(err) {
print("/invite Error: %s", err);
});
}
else if (line.indexOf("/file ") === 0) {
matrixClient.invite(viewingRoom.roomId, userId).then(
function () {
printMessages();
rl.prompt();
},
function (err) {
print("/invite Error: %s", err);
},
);
} else if (line.indexOf("/file ") === 0) {
var filename = line.split(" ")[1].trim();
var stream = fs.createReadStream(filename);
matrixClient.uploadContent({
stream: stream,
name: filename
}).then(function(url) {
var content = {
msgtype: "m.file",
body: filename,
url: JSON.parse(url).content_uri
};
matrixClient.sendMessage(viewingRoom.roomId, content);
});
}
else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function() {
matrixClient
.uploadContent({
stream: stream,
name: filename,
})
.then(function (url) {
var content = {
msgtype: "m.file",
body: filename,
url: JSON.parse(url).content_uri,
};
matrixClient.sendMessage(viewingRoom.roomId, content);
});
} else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
printMessages();
rl.prompt();
});
// print local echo immediately
printMessages();
}
}
else {
} else {
if (line.indexOf("/join ") === 0) {
var roomIndex = line.split(" ")[1];
viewingRoom = roomList[roomIndex];
if (viewingRoom.getMember(myUserId).membership === "invite") {
// join the room first
matrixClient.joinRoom(viewingRoom.roomId).then(function(room) {
setRoomList();
viewingRoom = room;
printMessages();
rl.prompt();
}, function(err) {
print("/join Error: %s", err);
});
}
else {
matrixClient.joinRoom(viewingRoom.roomId).then(
function (room) {
setRoomList();
viewingRoom = room;
printMessages();
rl.prompt();
},
function (err) {
print("/join Error: %s", err);
},
);
} else {
printMessages();
}
}
@@ -133,18 +138,18 @@ rl.on('line', function(line) {
// ==== END User input
// show the room list after syncing.
matrixClient.on("sync", function(state, prevState, data) {
matrixClient.on("sync", function (state, prevState, data) {
switch (state) {
case "PREPARED":
setRoomList();
printRoomList();
printHelp();
rl.prompt();
break;
}
setRoomList();
printRoomList();
printHelp();
rl.prompt();
break;
}
});
matrixClient.on("Room", function() {
matrixClient.on("Room", function () {
setRoomList();
if (!viewingRoom) {
printRoomList();
@@ -153,7 +158,7 @@ matrixClient.on("Room", function() {
});
// print incoming messages.
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
@@ -165,20 +170,19 @@ matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
function setRoomList() {
roomList = matrixClient.getRooms();
roomList.sort(function(a,b) {
roomList.sort(function (a, b) {
// < 0 = a comes first (lower index) - we want high indexes = newer
var aMsg = a.timeline[a.timeline.length-1];
var aMsg = a.timeline[a.timeline.length - 1];
if (!aMsg) {
return -1;
}
var bMsg = b.timeline[b.timeline.length-1];
var bMsg = b.timeline[b.timeline.length - 1];
if (!bMsg) {
return 1;
}
if (aMsg.getTs() > bMsg.getTs()) {
return 1;
}
else if (aMsg.getTs() < bMsg.getTs()) {
} else if (aMsg.getTs() < bMsg.getTs()) {
return -1;
}
return 0;
@@ -189,16 +193,15 @@ function printRoomList() {
print(CLEAR_CONSOLE);
print("Room List:");
var fmts = {
"invite": clc.cyanBright,
"leave": clc.blackBright
invite: clc.cyanBright,
leave: clc.blackBright,
};
for (var i = 0; i < roomList.length; i++) {
var msg = roomList[i].timeline[roomList[i].timeline.length-1];
var msg = roomList[i].timeline[roomList[i].timeline.length - 1];
var dateStr = "---";
var fmt;
if (msg) {
dateStr = new Date(msg.getTs()).toISOString().replace(
/T/, ' ').replace(/\..+/, '');
dateStr = new Date(msg.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
}
var myMembership = roomList[i].getMyMembership();
if (myMembership) {
@@ -207,9 +210,10 @@ function printRoomList() {
var roomName = fixWidth(roomList[i].name, 25);
print(
"[%s] %s (%s members) %s",
i, fmt ? fmt(roomName) : roomName,
i,
fmt ? fmt(roomName) : roomName,
roomList[i].getJoinedMembers().length,
dateStr
dateStr,
);
}
}
@@ -230,12 +234,12 @@ function printHelp() {
}
function completer(line) {
var completions = [
"/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"
];
var hits = completions.filter(function(c) { return c.indexOf(line) == 0 });
var completions = ["/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"];
var hits = completions.filter(function (c) {
return c.indexOf(line) == 0;
});
// show all completions if none found
return [hits.length ? hits : completions, line]
return [hits.length ? hits : completions, line];
}
function printMessages() {
@@ -252,14 +256,14 @@ function printMessages() {
function printMemberList(room) {
var fmts = {
"join": clc.green,
"ban": clc.red,
"invite": clc.blue,
"leave": clc.blackBright
join: clc.green,
ban: clc.red,
invite: clc.blue,
leave: clc.blackBright,
};
var members = room.currentState.getMembers();
// sorted based on name.
members.sort(function(a, b) {
members.sort(function (a, b) {
if (a.name > b.name) {
return -1;
}
@@ -268,21 +272,24 @@ function printMemberList(room) {
}
return 0;
});
print("Membership list for room \"%s\"", room.name);
print('Membership list for room "%s"', room.name);
print(new Array(room.name.length + 28).join("-"));
room.currentState.getMembers().forEach(function(member) {
room.currentState.getMembers().forEach(function (member) {
if (!member.membership) {
return;
}
var fmt = fmts[member.membership] || function(a){return a;};
var membershipWithPadding = (
member.membership + new Array(10 - member.membership.length).join(" ")
);
var fmt =
fmts[member.membership] ||
function (a) {
return a;
};
var membershipWithPadding = member.membership + new Array(10 - member.membership.length).join(" ");
print(
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
membershipWithPadding, member.name,
(member.userId === myUserId ? "Me" : member.userId),
fmt
"%s" + fmt(" :: ") + "%s" + fmt(" (") + "%s" + fmt(")"),
membershipWithPadding,
member.name,
member.userId === myUserId ? "Me" : member.userId,
fmt,
);
});
}
@@ -292,38 +299,31 @@ function printRoomInfo(room) {
var eTypeHeader = " Event Type(state_key) ";
var sendHeader = " Sender ";
// pad content to 100
var restCount = (
100 - "Content".length - " | ".length - " | ".length -
eTypeHeader.length - sendHeader.length
);
var padSide = new Array(Math.floor(restCount/2)).join(" ");
var restCount = 100 - "Content".length - " | ".length - " | ".length - eTypeHeader.length - sendHeader.length;
var padSide = new Array(Math.floor(restCount / 2)).join(" ");
var contentHeader = padSide + "Content" + padSide;
print(eTypeHeader+sendHeader+contentHeader);
print(eTypeHeader + sendHeader + contentHeader);
print(new Array(100).join("-"));
eventMap.keys().forEach(function(eventType) {
if (eventType === "m.room.member") { return; } // use /members instead.
eventMap.keys().forEach(function (eventType) {
if (eventType === "m.room.member") {
return;
} // use /members instead.
var eventEventMap = eventMap.get(eventType);
eventEventMap.keys().forEach(function(stateKey) {
var typeAndKey = eventType + (
stateKey.length > 0 ? "("+stateKey+")" : ""
);
eventEventMap.keys().forEach(function (stateKey) {
var typeAndKey = eventType + (stateKey.length > 0 ? "(" + stateKey + ")" : "");
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
var event = eventEventMap.get(stateKey);
var sendStr = fixWidth(event.getSender(), sendHeader.length);
var contentStr = fixWidth(
JSON.stringify(event.getContent()), contentHeader.length
);
print(typeStr+" | "+sendStr+" | "+contentStr);
var contentStr = fixWidth(JSON.stringify(event.getContent()), contentHeader.length);
print(typeStr + " | " + sendStr + " | " + contentStr);
});
})
});
}
function printLine(event) {
var fmt;
var name = event.sender ? event.sender.name : event.getSender();
var time = new Date(
event.getTs()
).toISOString().replace(/T/, ' ').replace(/\..+/, '');
var time = new Date(event.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
var separator = "<<<";
if (event.getSender() === myUserId) {
name = "Me";
@@ -331,8 +331,7 @@ function printLine(event) {
if (event.status === sdk.EventStatus.SENDING) {
separator = "...";
fmt = clc.xterm(8);
}
else if (event.status === sdk.EventStatus.NOT_SENT) {
} else if (event.status === sdk.EventStatus.NOT_SENT) {
separator = " x ";
fmt = clc.redBright;
}
@@ -341,69 +340,58 @@ function printLine(event) {
var maxNameWidth = 15;
if (name.length > maxNameWidth) {
name = name.slice(0, maxNameWidth-1) + "\u2026";
name = name.slice(0, maxNameWidth - 1) + "\u2026";
}
if (event.getType() === "m.room.message") {
body = event.getContent().body;
}
else if (event.isState()) {
} else if (event.isState()) {
var stateName = event.getType();
if (event.getStateKey().length > 0) {
stateName += " ("+event.getStateKey()+")";
stateName += " (" + event.getStateKey() + ")";
}
body = (
"[State: "+stateName+" updated to: "+JSON.stringify(event.getContent())+"]"
);
body = "[State: " + stateName + " updated to: " + JSON.stringify(event.getContent()) + "]";
separator = "---";
fmt = clc.xterm(249).italic;
}
else {
} else {
// random message event
body = (
"[Message: "+event.getType()+" Content: "+JSON.stringify(event.getContent())+"]"
);
body = "[Message: " + event.getType() + " Content: " + JSON.stringify(event.getContent()) + "]";
separator = "---";
fmt = clc.xterm(249).italic;
}
if (fmt) {
print(
"[%s] %s %s %s", time, name, separator, body, fmt
);
}
else {
print("[%s] %s %s %s", time, name, separator, body, fmt);
} else {
print("[%s] %s %s %s", time, name, separator, body);
}
}
function print(str, formatter) {
if (typeof arguments[arguments.length-1] === "function") {
if (typeof arguments[arguments.length - 1] === "function") {
// last arg is the formatter so get rid of it and use it on each
// param passed in but not the template string.
var newArgs = [];
var i = 0;
for (i=0; i<arguments.length-1; i++) {
for (i = 0; i < arguments.length - 1; i++) {
newArgs.push(arguments[i]);
}
var fmt = arguments[arguments.length-1];
for (i=0; i<newArgs.length; i++) {
var fmt = arguments[arguments.length - 1];
for (i = 0; i < newArgs.length; i++) {
newArgs[i] = fmt(newArgs[i]);
}
console.log.apply(console.log, newArgs);
}
else {
} else {
console.log.apply(console.log, arguments);
}
}
function fixWidth(str, len) {
if (str.length > len) {
return str.substring(0, len-2) + "\u2026";
}
else if (str.length < len) {
return str.substring(0, len - 2) + "\u2026";
} else if (str.length < len) {
return str + new Array(len - str.length).join(" ");
}
return str;
}
matrixClient.startClient(numMessagesToShow); // messages for each room.
matrixClient.startClient(numMessagesToShow); // messages for each room.
+12 -12
View File
@@ -1,14 +1,14 @@
{
"name": "example-app",
"version": "0.0.0",
"description": "",
"main": "app.js",
"scripts": {
"preinstall": "npm install ../.."
},
"author": "",
"license": "Apache 2.0",
"dependencies": {
"cli-color": "^1.0.0"
}
"name": "example-app",
"version": "0.0.0",
"description": "",
"main": "app.js",
"scripts": {
"preinstall": "npm install ../.."
},
"author": "",
"license": "Apache 2.0",
"dependencies": {
"cli-color": "^1.0.0"
}
}
+1 -1
View File
@@ -6,4 +6,4 @@ To try it out, **you must build the SDK first** and then host this folder:
$ python -m SimpleHTTPServer 8003
```
Then visit ``http://localhost:8003``.
Then visit `http://localhost:8003`.
+26 -23
View File
@@ -9,7 +9,7 @@ const client = matrixcs.createClient({
baseUrl: BASE_URL,
accessToken: TOKEN,
userId: USER_ID,
deviceId: DEVICE_ID
deviceId: DEVICE_ID,
});
let call;
@@ -21,18 +21,16 @@ function disableButtons(place, answer, hangup) {
function addListeners(call) {
let lastError = "";
call.on("hangup", function() {
call.on("hangup", function () {
disableButtons(false, true, true);
document.getElementById("result").innerHTML = (
"<p>Call ended. Last error: "+lastError+"</p>"
);
document.getElementById("result").innerHTML = "<p>Call ended. Last error: " + lastError + "</p>";
});
call.on("error", function(err) {
call.on("error", function (err) {
lastError = err.message;
call.hangup();
disableButtons(false, true, true);
});
call.on("feeds_changed", function(feeds) {
call.on("feeds_changed", function (feeds) {
const localFeed = feeds.find((feed) => feed.isLocal());
const remoteFeed = feeds.find((feed) => !feed.isLocal());
@@ -51,33 +49,38 @@ function addListeners(call) {
});
}
window.onload = function() {
window.onload = function () {
document.getElementById("result").innerHTML = "<p>Please wait. Syncing...</p>";
document.getElementById("config").innerHTML = "<p>" +
"Homeserver: <code>"+BASE_URL+"</code><br/>"+
"Room: <code>"+ROOM_ID+"</code><br/>"+
"User: <code>"+USER_ID+"</code><br/>"+
document.getElementById("config").innerHTML =
"<p>" +
"Homeserver: <code>" +
BASE_URL +
"</code><br/>" +
"Room: <code>" +
ROOM_ID +
"</code><br/>" +
"User: <code>" +
USER_ID +
"</code><br/>" +
"</p>";
disableButtons(true, true, true);
};
client.on("sync", function(state, prevState, data) {
client.on("sync", function (state, prevState, data) {
switch (state) {
case "PREPARED":
syncComplete();
break;
}
syncComplete();
break;
}
});
function syncComplete() {
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
disableButtons(false, true, true);
document.getElementById("call").onclick = function() {
document.getElementById("call").onclick = function () {
console.log("Placing call...");
call = matrixcs.createNewMatrixCall(
client, ROOM_ID
);
call = matrixcs.createNewMatrixCall(client, ROOM_ID);
console.log("Call => %s", call);
addListeners(call);
call.placeVideoCall();
@@ -85,14 +88,14 @@ function syncComplete() {
disableButtons(true, true, false);
};
document.getElementById("hangup").onclick = function() {
document.getElementById("hangup").onclick = function () {
console.log("Hanging up call...");
console.log("Call => %s", call);
call.hangup();
document.getElementById("result").innerHTML = "<p>Hungup call.</p>";
};
document.getElementById("answer").onclick = function() {
document.getElementById("answer").onclick = function () {
console.log("Answering call...");
console.log("Call => %s", call);
call.answer();
@@ -100,7 +103,7 @@ function syncComplete() {
document.getElementById("result").innerHTML = "<p>Answered call.</p>";
};
client.on("Call.incoming", function(c) {
client.on("Call.incoming", function (c) {
console.log("Call ringing");
disableButtons(true, false, false);
document.getElementById("result").innerHTML = "<p>Incoming call...</p>";
+19 -21
View File
@@ -1,25 +1,23 @@
<html>
<head>
<title>VoIP Test</title>
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<head>
<title>VoIP Test</title>
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
You can place and receive calls with this example. Make sure to edit the
constants in <code>browserTest.js</code> first.
<div id="config"></div>
<div id="result"></div>
<button id="call">Place Call</button>
<button id="answer">Answer Call</button>
<button id="hangup">Hangup Call</button>
<div id="videoBackground" class="video-background">
<video class="video-element" id="local"></video>
<video class="video-element" id="remote"></video>
</div>
</body>
<body>
You can place and receive calls with this example. Make sure to edit the constants in
<code>browserTest.js</code> first.
<div id="config"></div>
<div id="result"></div>
<button id="call">Place Call</button>
<button id="answer">Answer Call</button>
<button id="hangup">Hangup Call</button>
<div id="videoBackground" class="video-background">
<video class="video-element" id="local"></video>
<video class="video-element" id="remote"></video>
</div>
</body>
</html>
<style>
@@ -31,4 +29,4 @@
.video-element {
height: 100%;
}
</style>
</style>
+144 -137
View File
@@ -1,141 +1,148 @@
{
"name": "matrix-js-sdk",
"version": "21.2.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
},
"scripts": {
"prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "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": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src spec",
"lint:js-fix": "eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-js-sdk"
},
"keywords": [
"matrix-org"
],
"main": "./lib/index.js",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.js",
"matrix_lib_main": "./lib/index.js",
"matrix_lib_typings": "./lib/index.d.ts",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
"dist",
"lib",
"src",
"git-revision.txt",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"package.json",
"release.sh"
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/sdp-transform": "^2.4.5",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.0.0",
"p-retry": "4",
"qs": "^6.9.6",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@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/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": "^29.0.0",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"domexception": "^4.0.0",
"eslint": "8.26.0",
"eslint-config-google": "^0.14.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-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": {
"testEnvironment": "node",
"testMatch": [
"<rootDir>/spec/**/*.spec.{js,ts}"
"name": "matrix-js-sdk",
"version": "23.2.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
},
"scripts": {
"prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdir dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-js-sdk"
},
"keywords": [
"matrix-org"
],
"setupFilesAfterEnv": [
"<rootDir>/spec/setupTests.ts"
"main": "./lib/index.js",
"browser": "./src/browser-index.ts",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.ts",
"matrix_lib_main": "./lib/index.js",
"matrix_lib_browser": "./lib/browser-index.js",
"matrix_lib_typings": "./lib/index.d.ts",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
"dist",
"lib",
"src",
"git-revision.txt",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"package.json",
"release.sh"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts}"
],
"coverageReporters": [
"text-summary",
"lcov"
],
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"typings": "./lib/index.d.ts"
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.2",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.0.0",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
"uuid": "9"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@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.14.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/domexception": "^4.0.0",
"@types/jest": "^29.0.0",
"@types/node": "18",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "7",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^2.0.0",
"domexception": "^4.0.0",
"eslint": "8.31.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.6.4",
"eslint-plugin-matrix-org": "^0.9.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^45.0.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"matrix-mock-request": "^2.5.0",
"prettier": "2.8.2",
"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": {
"testEnvironment": "node",
"testMatch": [
"<rootDir>/spec/**/*.spec.{js,ts}"
],
"setupFilesAfterEnv": [
"<rootDir>/spec/setupTests.ts"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts}"
],
"coverageReporters": [
"text-summary",
"lcov"
],
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"typings": "./lib/index.d.ts"
}
+2 -2
View File
@@ -21,9 +21,9 @@ if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
# 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
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
fi
done
+1 -1
View File
@@ -184,7 +184,7 @@ for i in main typings
do
lib_value=$(jq -r ".matrix_lib_$i" package.json)
if [ "$lib_value" != "null" ]; then
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
done
+6 -6
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env node
const fsProm = require('fs/promises');
const fsProm = require("fs/promises");
const PKGJSON = 'package.json';
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];
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));
+70 -52
View File
@@ -17,20 +17,20 @@ limitations under the License.
*/
// load olm before the sdk if possible
import './olm-loader';
import "./olm-loader";
import MockHttpBackend from 'matrix-mock-request';
import MockHttpBackend from "matrix-mock-request";
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
import { logger } from '../src/logger';
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
import { logger } from "../src/logger";
import { syncPromise } from "./test-utils/test-utils";
import { createClient } from "../src/matrix";
import { createClient, IStartClientOpts } from "../src/matrix";
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils";
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
import { IKeyBackupSession } from "../src/crypto/keybackup";
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
@@ -73,15 +73,18 @@ export class TestClient {
}
public toString(): string {
return 'TestClient[' + this.userId + ']';
return "TestClient[" + this.userId + "]";
}
/**
* start the client, and wait for it to initialise.
*/
public start(): Promise<void> {
logger.log(this + ': starting');
this.httpBackend.when("GET", "/versions").respond(200, {});
public start(opts: IStartClientOpts = {}): Promise<void> {
logger.log(this + ": starting");
this.httpBackend.when("GET", "/versions").respond(200, {
// we have tests that rely on support for lazy-loading members
versions: ["r0.5.0"],
});
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
@@ -93,19 +96,18 @@ export class TestClient {
this.client.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: PendingEventOrdering.Detached,
...opts,
});
return Promise.all([
this.httpBackend.flushAllExpected(),
syncPromise(this.client),
]).then(() => {
logger.log(this + ': started');
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
logger.log(this + ": started");
});
}
/**
* stop the client
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
* @returns Promise which resolves once the mock http backend has finished all pending flushes
*/
public async stop(): Promise<void> {
this.client.stopClient();
@@ -113,20 +115,30 @@ export class TestClient {
}
/**
* Set up expectations that the client will upload device keys.
* Set up expectations that the client will upload device keys (and possibly one-time keys)
*/
public expectDeviceKeyUpload() {
this.httpBackend.when("POST", "/keys/upload")
this.httpBackend
.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
logger.log(this + ': received device keys');
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);
this.deviceKeys = content.device_keys;
return { one_time_key_counts: { signed_curve25519: 0 } };
// the first batch of one-time keys may be uploaded at the same time.
if (content.one_time_keys) {
logger.log(`${this}: received ${Object.keys(content.one_time_keys).length} one-time keys`);
this.oneTimeKeys = content.one_time_keys;
}
return {
one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
},
};
});
}
@@ -135,7 +147,7 @@ export class TestClient {
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns {Promise} for the one-time keys
* @returns Promise for the one-time keys
*/
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
if (Object.keys(this.oneTimeKeys!).length != 0) {
@@ -143,30 +155,35 @@ export class TestClient {
return Promise.resolve(this.oneTimeKeys!);
}
this.httpBackend.when("POST", "/keys/upload")
this.httpBackend
.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
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,
} };
return {
one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
},
};
});
this.httpBackend.when("POST", "/keys/upload")
this.httpBackend
.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
expect(content.device_keys).toBe(undefined);
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);
logger.log("%s: received %i one-time keys", this, 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,
} };
return {
one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
},
};
});
// this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
return this.httpBackend.flush("/keys/upload", 2, 1000).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys!;
});
@@ -177,45 +194,49 @@ export class TestClient {
*
* We check that the query contains each of the users in `response`.
*
* @param {Object} response response to the query.
* @param response - response to the query.
*/
public expectKeyQuery(response: IDownloadKeyResult) {
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
200, (_path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys![userId]).toEqual([]);
});
return response;
this.httpBackend.when("POST", "/keys/query").respond<IDownloadKeyResult>(200, (_path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect((content.device_keys! as Record<string, any>)[userId]).toEqual([]);
});
return response;
});
}
/**
* Set up expectations that the client will query key backups for a particular session
*/
public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) {
this.httpBackend.when('GET', encodeUri("/room_keys/keys/$roomId/$sessionId", {
$roomId: roomId,
$sessionId: sessionId,
})).respond(status, response);
this.httpBackend
.when(
"GET",
encodeUri("/room_keys/keys/$roomId/$sessionId", {
$roomId: roomId,
$sessionId: sessionId,
}),
)
.respond(status, response);
}
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
* @returns base64 device key
*/
public getDeviceKey(): string {
const keyId = 'curve25519:' + this.deviceId;
const keyId = "curve25519:" + this.deviceId;
return this.deviceKeys!.keys[keyId];
}
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
* @returns base64 device key
*/
public getSigningKey(): string {
const keyId = 'ed25519:' + this.deviceId;
const keyId = "ed25519:" + this.deviceId;
return this.deviceKeys!.keys[keyId];
}
@@ -224,10 +245,7 @@ export class TestClient {
*/
public flushSync(): Promise<void> {
logger.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
syncPromise(this.client),
]).then(() => {
return Promise.all([this.httpBackend.flush("/sync", 1), syncPromise(this.client)]).then(() => {
logger.log(`${this}: flushSync completed`);
});
}
+2 -14
View File
@@ -15,19 +15,7 @@ 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;
};
}
}
}
import type { default as BrowserMatrix } from "../../src/browser-index";
// stub for browser-matrix browserify tests
// @ts-ignore
@@ -43,4 +31,4 @@ afterAll(() => {
global.matrixcs = {
...global.matrixcs,
timeoutSignal: () => new AbortController().signal,
};
} as typeof BrowserMatrix;
+11 -12
View File
@@ -16,7 +16,7 @@ limitations under the License.
import HttpBackend from "matrix-mock-request";
import "./setupTests";// uses browser-matrix instead of the src
import "./setupTests"; // uses browser-matrix instead of the src
import type { MatrixClient } from "../../src";
const USER_ID = "@user:test.server";
@@ -24,7 +24,7 @@ const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
describe("Browserify Test", function() {
describe("Browserify Test", function () {
let client: MatrixClient;
let httpBackend: HttpBackend;
@@ -65,22 +65,21 @@ describe("Browserify Test", function() {
const syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[ROOM_ID] = {
timeline: {
events: [
event,
],
limited: false,
join: {
[ROOM_ID]: {
timeline: {
events: [event],
limited: false,
},
},
},
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, syncData);
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
const syncPromise = new Promise((r) => client.once(global.matrixcs.ClientEvent.Sync, r));
const unexpectedErrorFn = jest.fn();
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
File diff suppressed because it is too large Load Diff
+264 -259
View File
@@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { TestClient } from '../TestClient';
import * as testUtils from '../test-utils/test-utils';
import { logger } from '../../src/logger';
import { TestClient } from "../TestClient";
import * as testUtils from "../test-utils/test-utils";
import { logger } from "../../src/logger";
const ROOM_ID = "!room:id";
@@ -26,26 +26,24 @@ const ROOM_ID = "!room:id";
* get a /sync response which contains a single e2e room (ROOM_ID), with the
* members given
*
* @param {string[]} roomMembers
*
* @return {object} sync response
* @returns sync response
*/
function getSyncResponse(roomMembers) {
function getSyncResponse(roomMembers: string[]) {
const stateEvents = [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
type: "m.room.encryption",
skey: "",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
algorithm: "m.megolm.v1.aes-sha2",
},
}),
];
Array.prototype.push.apply(
stateEvents,
roomMembers.map(
(m) => testUtils.mkMembership({
mship: 'join',
roomMembers.map((m) =>
testUtils.mkMembership({
mship: "join",
sender: m,
}),
),
@@ -67,24 +65,22 @@ function getSyncResponse(roomMembers) {
return syncResponse;
}
describe("DeviceList management:", function() {
describe("DeviceList management:", function () {
if (!global.Olm) {
logger.warn('not running deviceList tests: Olm not present');
logger.warn("not running deviceList tests: Olm not present");
return;
}
let sessionStoreBackend;
let aliceTestClient;
let aliceTestClient: TestClient;
let sessionStoreBackend: Storage;
async function createTestClient() {
const testClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
);
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
await testClient.client.initCrypto();
return testClient;
}
beforeEach(async function() {
beforeEach(async function () {
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.
sessionStoreBackend = new testUtils.MockStorageApi();
@@ -92,305 +88,314 @@ describe("DeviceList management:", function() {
aliceTestClient = await createTestClient();
});
afterEach(function() {
afterEach(function () {
return aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
logger.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
logger.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
it("Alice shouldn't do a second /query for non-e2e-capable devices", function () {
aliceTestClient.expectKeyQuery({
device_keys: { "@alice:localhost": {} },
failures: {},
});
return aliceTestClient
.start()
.then(function () {
const syncResponse = getSyncResponse(["@bob:xyz"]);
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
})
.then(function () {
logger.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: {
"@bob:xyz": {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(["@bob:xyz"]),
aliceTestClient.httpBackend.flush("/keys/query", 1),
]);
})
.then(function () {
logger.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, {
event_id: "$event_id",
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it.skip("We should not get confused by out-of-order device query responses", () => {
// https://github.com/vector-im/element-web/issues/3126
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
return aliceTestClient.flushSync();
}).then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
'@chris:abc': {},
},
},
);
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
200, { event_id: '$event1' });
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
]);
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
expect(data.syncToken).toEqual(1);
});
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '2',
device_lists: {
changed: ['@bob:xyz'],
},
});
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '3',
device_lists: {
changed: ['@chris:abc'],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@chris:abc': {},
},
token: '3',
}).respond(200, {
device_keys: { '@chris:abc': {} },
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
);
}
});
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@bob:xyz': {},
},
token: '2',
}).respond(200, {
device_keys: { '@bob:xyz': {} },
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
);
}
});
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
const chrisStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data.syncToken).toEqual(3);
});
aliceTestClient.expectKeyQuery({
device_keys: { "@alice:localhost": {} },
failures: {},
});
return aliceTestClient
.start()
.then(() => {
aliceTestClient.httpBackend
.when("GET", "/sync")
.respond(200, getSyncResponse(["@bob:xyz", "@chris:abc"]));
return aliceTestClient.flushSync();
})
.then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: {
"@bob:xyz": {},
"@chris:abc": {},
},
});
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { event_id: "$event1" });
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
aliceTestClient.httpBackend
.flush("/keys/query", 1)
.then(() => aliceTestClient.httpBackend.flush("/send/", 1)),
aliceTestClient.client.crypto!.deviceList.saveIfDirty(),
]);
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
expect(data!.syncToken).toEqual(1);
});
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: "2",
device_lists: {
changed: ["@bob:xyz"],
},
});
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: "3",
device_lists: {
changed: ["@chris:abc"],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
})
.then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend
.when("POST", "/keys/query", {
device_keys: {
"@chris:abc": {},
},
token: "3",
})
.respond(200, {
device_keys: { "@chris:abc": {} },
});
return aliceTestClient.httpBackend.flush("/keys/query", 1);
})
.then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
if (bobStat != 1 && bobStat != 2) {
throw new Error("Unexpected status for bob: wanted 1 or 2, got " + bobStat);
}
const chrisStat = data!.trackingStatus["@chris:abc"];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + chrisStat);
}
});
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend
.when("POST", "/keys/query", {
device_keys: {
"@bob:xyz": {},
},
token: "2",
})
.respond(200, {
device_keys: { "@bob:xyz": {} },
});
return aliceTestClient.httpBackend.flush("/keys/query", 1);
})
.then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(["@bob:xyz"]);
})
.then(() => {
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
expect(bobStat).toEqual(3);
const chrisStat = data!.trackingStatus["@chris:abc"];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + bobStat);
}
});
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush("/keys/query", 1);
})
.then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(["@chris:abc"]);
})
.then(() => {
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
const chrisStat = data!.trackingStatus["@bob:xyz"];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data!.syncToken).toEqual(3);
});
});
});
// https://github.com/vector-im/element-web/issues/4983
describe("Alice should know she has stale device lists", () => {
beforeEach(async function() {
beforeEach(async function () {
await aliceTestClient.start();
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz']));
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"]));
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
},
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: {
"@bob:xyz": {},
},
);
await aliceTestClient.httpBackend.flush('/keys/query', 1);
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
});
await aliceTestClient.httpBackend.flush("/keys/query", 1);
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan(
0,
);
expect(bobStat).toBeGreaterThan(0);
});
});
it("when Bob leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
it("when Bob leaves", async function () {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
device_lists: {
left: ["@bob:xyz"],
},
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: "leave",
sender: "@bob:xyz",
}),
],
},
},
},
},
);
});
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
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,
);
expect(bobStat).toEqual(0);
});
});
it("when Alice leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
leave: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
it("when Alice leaves", async function () {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
device_lists: {
left: ["@bob:xyz"],
},
rooms: {
leave: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: "leave",
sender: "@bob:xyz",
}),
],
},
},
},
},
);
});
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
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,
);
expect(bobStat).toEqual(0);
});
});
it("when Bob leaves whilst Alice is offline", async function() {
it("when Bob leaves whilst Alice is offline", async function () {
aliceTestClient.stop();
const anotherTestClient = await createTestClient();
try {
await anotherTestClient.start();
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
anotherTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
await anotherTestClient.flushSync();
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,
);
expect(bobStat).toEqual(0);
});
} finally {
anotherTestClient.stop();
+85 -87
View File
@@ -29,7 +29,7 @@ import {
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient events", function() {
describe("MatrixClient events", function () {
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
let client: MatrixClient | undefined;
@@ -46,23 +46,25 @@ describe("MatrixClient events", function() {
return [client!, httpBackend];
};
beforeEach(function() {
beforeEach(function () {
[client!, httpBackend] = setupTests();
});
afterEach(function() {
afterEach(function () {
httpBackend?.verifyNoOutstandingExpectation();
client?.stopClient();
return httpBackend?.stop();
});
describe("emissions", function() {
describe("emissions", function () {
const SYNC_DATA = {
next_batch: "s_5_3",
presence: {
events: [
utils.mkPresence({
user: "@foo:bar", name: "Foo Bar", presence: "online",
user: "@foo:bar",
name: "Foo Bar",
presence: "online",
}),
],
},
@@ -72,7 +74,9 @@ describe("MatrixClient events", function() {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
room: "!erufh:bar",
user: "@foo:bar",
msg: "hmmm",
}),
],
prev_batch: "s",
@@ -80,10 +84,13 @@ describe("MatrixClient events", function() {
state: {
events: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar",
room: "!erufh:bar",
mship: "join",
user: "@foo:bar",
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar",
type: "m.room.create",
room: "!erufh:bar",
user: "@foo:bar",
content: {
creator: "@foo:bar",
@@ -103,18 +110,23 @@ describe("MatrixClient events", function() {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar",
room: "!erufh:bar",
user: "@foo:bar",
msg: "ello ello",
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
room: "!erufh:bar",
user: "@foo:bar",
msg: ":D",
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
type: "m.typing",
room: "!erufh:bar",
content: {
user_ids: ["@foo:bar"],
},
}),
@@ -125,50 +137,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);
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);
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,
);
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(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;
}
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);
});
client!.startClient();
return Promise.all([
// wait for two SYNCING events
utils.syncPromise(client!).then(() => {
return utils.syncPromise(client!);
}),
httpBackend!.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(0);
});
}
expect(found).toBe(true);
});
it("should emit User events", function(done) {
client!.startClient();
return Promise.all([
// wait for two SYNCING events
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);
let fired = false;
client!.on(UserEvent.Presence, function(event, user) {
client!.on(UserEvent.Presence, function (event, user) {
fired = true;
expect(user).toBeTruthy();
expect(event).toBeTruthy();
@@ -177,59 +188,52 @@ describe("MatrixClient events", function() {
}
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
SYNC_DATA.presence.events[0]?.content?.presence,
);
expect(user.presence).toEqual(SYNC_DATA.presence.events[0]?.content?.presence);
});
client!.startClient();
httpBackend!.flushAllExpected().then(function() {
httpBackend!.flushAllExpected().then(function () {
expect(fired).toBe(true);
done();
});
});
it("should emit Room events", function() {
it("should emit Room events", function () {
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(ClientEvent.Room, function(room) {
client!.on(ClientEvent.Room, function (room) {
roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar");
});
client!.on(RoomEvent.Timeline, function(event, room) {
client!.on(RoomEvent.Timeline, function (event, room) {
timelineFireCount++;
expect(room?.roomId).toEqual("!erufh:bar");
});
client!.on(RoomEvent.Name, function(room) {
client!.on(RoomEvent.Name, function (room) {
roomNameInvokeCount++;
});
client!.startClient();
return Promise.all([
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
expect(roomInvokeCount).toEqual(1);
expect(roomNameInvokeCount).toEqual(1);
expect(timelineFireCount).toEqual(3);
});
});
it("should emit RoomState events", function() {
it("should emit RoomState events", function () {
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",
];
const roomStateEventTypes = ["m.room.member", "m.room.create"];
let eventsInvokeCount = 0;
let membersInvokeCount = 0;
let newMemberInvokeCount = 0;
client!.on(RoomStateEvent.Events, function(event, state) {
client!.on(RoomStateEvent.Events, function (event, state) {
eventsInvokeCount++;
const index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(-1);
@@ -237,13 +241,13 @@ describe("MatrixClient events", function() {
roomStateEventTypes.splice(index, 1);
}
});
client!.on(RoomStateEvent.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(RoomStateEvent.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");
@@ -252,17 +256,14 @@ describe("MatrixClient events", function() {
client!.startClient();
return Promise.all([
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
expect(membersInvokeCount).toEqual(1);
expect(newMemberInvokeCount).toEqual(1);
expect(eventsInvokeCount).toEqual(2);
});
});
it("should emit RoomMember events", function() {
it("should emit RoomMember events", function () {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
@@ -270,27 +271,24 @@ describe("MatrixClient events", function() {
let powerLevelInvokeCount = 0;
let nameInvokeCount = 0;
let membershipInvokeCount = 0;
client!.on(RoomMemberEvent.Name, function(event, member) {
client!.on(RoomMemberEvent.Name, function (event, member) {
nameInvokeCount++;
});
client!.on(RoomMemberEvent.Typing, function(event, member) {
client!.on(RoomMemberEvent.Typing, function (event, member) {
typingInvokeCount++;
expect(member.typing).toBe(true);
});
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
client!.on(RoomMemberEvent.PowerLevel, function (event, member) {
powerLevelInvokeCount++;
});
client!.on(RoomMemberEvent.Membership, function(event, member) {
client!.on(RoomMemberEvent.Membership, function (event, member) {
membershipInvokeCount++;
expect(member.membership).toEqual("join");
});
client!.startClient();
return Promise.all([
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
expect(typingInvokeCount).toEqual(1);
expect(powerLevelInvokeCount).toEqual(0);
expect(nameInvokeCount).toEqual(0);
@@ -298,36 +296,36 @@ describe("MatrixClient events", function() {
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN' };
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function () {
const error = { errcode: "M_UNKNOWN_TOKEN" };
httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client!.startClient();
return httpBackend!.flushAllExpected().then(function() {
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 };
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);
let sessionLoggedOutCount = 0;
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client!.startClient();
return httpBackend!.flushAllExpected().then(function() {
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
+69 -55
View File
@@ -1,13 +1,13 @@
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { MatrixClient } from "../../src/matrix";
import { ClientEvent, 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() {
describe("MatrixClient opts", function () {
const baseUrl = "http://localhost.or.something";
let httpBackend = new HttpBackend();
const userId = "@alice:localhost";
@@ -19,11 +19,14 @@ describe("MatrixClient opts", function() {
presence: {},
rooms: {
join: {
"!foo:bar": { // roomId
"!foo:bar": {
// roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello",
room: roomId,
user: userB,
msg: "hello",
}),
],
prev_batch: "f_1_1",
@@ -31,19 +34,29 @@ describe("MatrixClient opts", function() {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
type: "m.room.name",
room: roomId,
user: userB,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob",
room: roomId,
mship: "join",
user: userB,
name: "Bob",
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice",
room: roomId,
mship: "join",
user: userId,
name: "Alice",
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
type: "m.room.create",
room: roomId,
user: userId,
content: {
creator: userId,
},
@@ -55,18 +68,18 @@ describe("MatrixClient opts", function() {
},
};
beforeEach(function() {
beforeEach(function () {
httpBackend = new HttpBackend();
});
afterEach(function() {
afterEach(function () {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
describe("without opts.store", function() {
let client;
beforeEach(function() {
describe("without opts.store", function () {
let client: MatrixClient;
beforeEach(function () {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: undefined,
@@ -77,34 +90,34 @@ describe("MatrixClient opts", function() {
});
});
afterEach(function() {
afterEach(function () {
client.stopClient();
});
it("should be able to send messages", function(done) {
it("should be able to send messages", function (done) {
const eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
expect(res.event_id).toEqual(eventId);
done();
});
httpBackend.flush("/txn1", 1);
});
it("should be able to sync / get new events", async function() {
const expectedEventTypes = [ // from /initialSync
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
it("should be able to sync / get new events", async function () {
const expectedEventTypes = [
// from /initialSync
"m.room.message",
"m.room.name",
"m.room.member",
"m.room.member",
"m.room.create",
];
client.on("event", function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1,
);
expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1,
);
client.on(ClientEvent.Event, function (event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(-1);
expectedEventTypes.splice(expectedEventTypes.indexOf(event.getType()), 1);
});
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
@@ -114,19 +127,14 @@ describe("MatrixClient opts", function() {
await httpBackend.flush("/versions", 1);
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0,
);
await Promise.all([httpBackend.flush("/sync", 1), utils.syncPromise(client)]);
expect(expectedEventTypes.length).toEqual(0);
});
});
describe("without opts.scheduler", function() {
let client;
beforeEach(function() {
describe("without opts.scheduler", function () {
let client: MatrixClient;
beforeEach(function () {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: new MemoryStore() as IStore,
@@ -137,25 +145,31 @@ describe("MatrixClient opts", function() {
});
});
afterEach(function() {
afterEach(function () {
client.stopClient();
});
it("shouldn't retry sending events", function(done) {
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);
}, function(err) {
expect(err.errcode).toEqual("M_SOMETHING");
done();
});
it("shouldn't retry sending events", function (done) {
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);
},
function (err) {
expect(err.errcode).toEqual("M_SOMETHING");
done();
},
);
httpBackend.flush("/txn1", 1);
});
it("shouldn't queue events", function(done) {
it("shouldn't queue events", function (done) {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "AAA",
});
@@ -164,26 +178,26 @@ describe("MatrixClient opts", function() {
});
let sentA = false;
let sentB = false;
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
sentA = true;
expect(sentB).toBe(true);
});
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function(res) {
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
sentB = true;
expect(sentA).toBe(false);
});
httpBackend.flush("/txn2", 1).then(function() {
httpBackend.flush("/txn1", 1).then(function() {
httpBackend.flush("/txn2", 1).then(function () {
httpBackend.flush("/txn1", 1).then(function () {
done();
});
});
});
it("should be able to send messages", function(done) {
it("should be able to send messages", function (done) {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "foo",
});
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
expect(res.event_id).toEqual("foo");
done();
});
+28 -39
View File
@@ -29,13 +29,7 @@ describe("MatrixClient relations", () => {
const setupTests = (): [MatrixClient, HttpBackend] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ scheduler },
);
const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
const httpBackend = testClient.httpBackend;
const client = testClient.client;
@@ -52,76 +46,71 @@ describe("MatrixClient relations", () => {
});
it("should read related events with the default options", async () => {
const response = client!.relations(roomId, '$event-0', null, null);
const response = client!.relations(roomId, "$event-0", null, null);
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
.when("GET", "/_matrix/client/v1/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 });
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);
const response = client!.relations(roomId, "$event-0", "m.reference", null);
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
.when("GET", "/_matrix/client/v1/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 });
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');
const response = client!.relations(roomId, "$event-0", "m.reference", "m.room.message");
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
.when("GET", "/_matrix/client/v1/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 });
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, {
const response = client!.relations(roomId, "$event-0", null, null, {
dir: Direction.Forward,
from: 'FROM',
from: "FROM",
limit: 10,
to: 'TO',
to: "TO",
});
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
.when("GET", "/_matrix/client/v1/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 });
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);
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' });
.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" });
expect(await response).toEqual({ chunk: [], next_batch: "NEXT" });
});
});
+45 -59
View File
@@ -20,7 +20,7 @@ import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() {
describe("MatrixClient retrying", function () {
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
@@ -30,13 +30,7 @@ describe("MatrixClient retrying", function() {
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ scheduler },
);
const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
const httpBackend = testClient.httpBackend;
const client = testClient.client;
const room = new Room(roomId, client, userId);
@@ -45,49 +39,46 @@ describe("MatrixClient retrying", function() {
return [client, httpBackend, room];
};
beforeEach(function() {
beforeEach(function () {
[client, httpBackend, room] = setupTests();
});
afterEach(function() {
afterEach(function () {
httpBackend!.verifyNoOutstandingExpectation();
return httpBackend!.stop();
});
xit("should retry according to MatrixScheduler.retryFn", function() {
xit("should retry according to MatrixScheduler.retryFn", function () {});
});
xit("should queue according to MatrixScheduler.queueFn", function () {});
xit("should queue according to MatrixScheduler.queueFn", function() {
xit("should mark events as EventStatus.NOT_SENT when giving up", function () {});
});
xit("should mark events as EventStatus.QUEUED when queued", function () {});
xit("should mark events as EventStatus.NOT_SENT when giving up", function() {
});
xit("should mark events as EventStatus.QUEUED when queued", function() {
});
it("should mark events as EventStatus.CANCELLED when cancelled", 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, {
"msgtype": "m.text",
"body": "m1",
}).then(function() {
// we expect the first message to fail
throw new Error('Message 1 unexpectedly sent successfully');
}, () => {
// this is expected
});
const p1 = client!
.sendMessage(roomId, {
msgtype: "m.text",
body: "m1",
})
.then(
function () {
// we expect the first message to fail
throw new Error("Message 1 unexpectedly sent successfully");
},
() => {
// this is expected
},
);
// 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, {
"msgtype": "m.text",
"body": "m2",
msgtype: "m.text",
body: "m2",
});
// both events should be in the timeline at this point
@@ -100,20 +91,23 @@ 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() {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
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);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// now we can cancel the second and check everything looks sane
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);
}).toThrow();
}).respond(400); // fail the first message
// shouldn't be able to cancel the first message yet
expect(function () {
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) => {
@@ -122,7 +116,7 @@ describe("MatrixClient retrying", function() {
resolve();
}
});
}).then(function() {
}).then(function () {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);
@@ -132,19 +126,11 @@ describe("MatrixClient retrying", function() {
expect(tl.length).toEqual(0);
});
return Promise.all([
p1,
p3,
httpBackend!.flushAllExpected(),
]);
return Promise.all([p1, p3, httpBackend!.flushAllExpected()]);
});
describe("resending", function() {
xit("should be able to resend a NOT_SENT event", function() {
});
xit("should be able to resend a sent event", function() {
});
describe("resending", function () {
xit("should be able to resend a NOT_SENT event", function () {});
xit("should be able to resend a sent event", function () {});
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+68 -68
View File
@@ -23,22 +23,23 @@ import { TestClient } from "../TestClient";
import { IEvent } from "../../src";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
const ROOM_ID = '!ROOM:ID';
const ROOM_ID = "!ROOM:ID";
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
const SESSION_ID = "o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc";
const ENCRYPTED_EVENT: Partial<IEvent> = {
type: 'm.room.encrypted',
type: "m.room.encrypted",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: 'SENDER_CURVE25519',
algorithm: "m.megolm.v1.aes-sha2",
sender_key: "SENDER_CURVE25519",
session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
ciphertext:
"AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" +
"CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" +
"mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs",
},
room_id: '!ROOM:ID',
event_id: '$event1',
room_id: "!ROOM:ID",
event_id: "$event1",
origin_server_ts: 1507753886000,
};
@@ -47,19 +48,20 @@ const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
ciphertext:
"2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" +
"6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" +
"Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" +
"SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" +
"Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" +
"ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" +
"4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" +
"C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" +
"Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" +
"QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" +
"iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg",
mac: "5lxYBHQU80M",
ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14",
},
};
@@ -82,16 +84,14 @@ function createOlmSession(olmAccount: Olm.Account, recipientTestClient: TestClie
const otk = keys[otkId];
const session = new global.Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
);
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
return session;
});
}
describe("megolm key backups", function() {
describe("megolm key backups", function () {
if (!global.Olm) {
logger.warn('not running megolm tests: Olm not present');
logger.warn("not running megolm tests: Olm not present");
return;
}
const Olm = global.Olm;
@@ -99,72 +99,72 @@ describe("megolm key backups", function() {
let aliceTestClient: TestClient;
const setupTestClient = (): [Account, TestClient] => {
const aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
const aliceTestClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs");
const testOlmAccount = new Olm.Account();
testOlmAccount!.create();
return [testOlmAccount, aliceTestClient];
};
beforeAll(function() {
beforeAll(function () {
return Olm.init();
});
beforeEach(async function() {
beforeEach(async function () {
[testOlmAccount, aliceTestClient] = setupTestClient();
await aliceTestClient!.client.initCrypto();
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
});
afterEach(function() {
afterEach(function () {
return aliceTestClient!.stop();
});
it("Alice checks key backups when receiving a message she can't decrypt", function() {
it("Alice checks key backups when receiving a message she can't decrypt", function () {
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [ENCRYPTED_EVENT],
join: {
[ROOM_ID]: {
timeline: {
events: [ENCRYPTED_EVENT],
},
},
},
},
};
return aliceTestClient!.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then(() => {
const privkey = decodeRecoveryKey(RECOVERY_KEY);
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
}).then(() => {
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient!.expectKeyBackupQuery(
ROOM_ID,
SESSION_ID,
200,
CURVE25519_KEY_BACKUP_DATA,
);
return aliceTestClient!.httpBackend.flushAllExpected();
}).then(function(): Promise<MatrixEvent> {
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
return aliceTestClient!
.start()
.then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
})
.then(() => {
const privkey = decodeRecoveryKey(RECOVERY_KEY);
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
})
.then(() => {
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient!.expectKeyBackupQuery(ROOM_ID, SESSION_ID, 200, CURVE25519_KEY_BACKUP_DATA);
return aliceTestClient!.httpBackend.flushAllExpected();
})
.then(function (): Promise<MatrixEvent> {
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
if (event.getContent()) {
return Promise.resolve(event);
}
if (event.getContent()) {
return Promise.resolve(event);
}
return new Promise((resolve, reject) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
return new Promise((resolve, reject) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
})
.then((event) => {
expect(event.getContent()).toEqual("testytest");
});
}).then((event) => {
expect(event.getContent()).toEqual('testytest');
});
});
});
File diff suppressed because it is too large Load Diff
@@ -22,18 +22,20 @@ limitations under the License.
*
* Note that megolm (group) conversation is not tested here.
*
* See also `megolm.spec.js`.
* See also `crypto.spec.js`.
*/
// load olm before the sdk if possible
import '../olm-loader';
import "../olm-loader";
import { logger } from '../../src/logger';
import type { Session } from "@matrix-org/olm";
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 { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from '../../src/crypto/deviceinfo';
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration";
let aliTestClient: TestClient;
const roomId = "!room:localhost";
@@ -47,39 +49,31 @@ 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;
}
type OlmPayload = ReturnType<Session["encrypt"]>;
async function bobUploadsDeviceKeys(): Promise<void> {
bobTestClient.expectDeviceKeyUpload();
await Promise.all([
bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flushAllExpected(),
]);
await 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.
* @returns 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 };
});
const uploaderKeys: Record<string, IDeviceKeys> = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!;
querier.httpBackend.when("POST", "/keys/query").respond(200, function (_path, content: IQueryKeysRequest) {
expect(content.device_keys![uploader.userId!]).toEqual([]);
const result: Record<string, Record<string, IDeviceKeys>> = {};
result[uploader.userId!] = uploaderKeys;
return { device_keys: result };
});
return querier.httpBackend.flush("/keys/query", 1);
}
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
@@ -88,16 +82,14 @@ 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.
* @returns 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) {
aliTestClient.httpBackend.when("POST", "/keys/claim").respond(200, function (_path, content: IClaimKeysRequest) {
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = '';
let keyId = "";
for (keyId in keys) {
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
@@ -105,7 +97,7 @@ async function expectAliClaimKeys(): Promise<void> {
}
}
}
const result = {};
const result: Record<string, Record<string, Record<string, IOneTimeKey>>> = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
@@ -138,8 +130,7 @@ async function aliDownloadsKeys(): Promise<void> {
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);
expect(devices[bobDeviceId].verified).toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
});
}
@@ -156,15 +147,13 @@ 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.
* @returns 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),
expectAliQueryKeys().then(expectAliClaimKeys).then(expectAliSendMessageRequest),
]);
return ciphertext;
}
@@ -173,14 +162,11 @@ async function aliSendsFirstMessage(): Promise<OlmPayload> {
* 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.
* @returns 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(),
]);
const [_, ciphertext] = await Promise.all([sendMessage(aliTestClient.client), expectAliSendMessageRequest()]);
return ciphertext;
}
@@ -188,14 +174,13 @@ async function aliSendsMessage(): Promise<OlmPayload> {
* 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.
* @returns 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),
expectBobQueryKeys().then(expectBobSendMessageRequest),
]);
return ciphertext;
}
@@ -203,7 +188,7 @@ async function bobSendsReplyMessage(): Promise<OlmPayload> {
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
@@ -217,7 +202,7 @@ async function expectAliSendMessageRequest(): Promise<OlmPayload> {
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
@@ -231,15 +216,13 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
}
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
return client.sendMessage(
roomId, { msgtype: "m.text", body: "Hello, World" },
);
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) {
httpBackend.when("PUT", path).respond(200, function (_path, content) {
resolve(content);
return {
event_id: "asdfgh",
@@ -254,16 +237,12 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
function aliRecvMessage(): Promise<void> {
const message = bobMessages.shift()!;
return recvMessage(
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
);
return recvMessage(aliTestClient.httpBackend, aliTestClient.client, bobUserId, message);
}
function bobRecvMessage(): Promise<void> {
const message = aliMessages.shift()!;
return recvMessage(
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
);
return recvMessage(bobTestClient.httpBackend, bobTestClient.client, aliUserId, message);
}
async function recvMessage(
@@ -276,32 +255,30 @@ async function recvMessage(
next_batch: "x",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
},
},
},
};
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) {
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);
logger.log(client.credentials.userId + " received event", event);
client.removeListener(ClientEvent.Event, onEvent);
resolve(event);
@@ -327,32 +304,32 @@ async function recvMessage(
* 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.
* @returns 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: [],
join: {
[roomId]: {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
},
},
},
},
};
@@ -385,6 +362,13 @@ describe("MatrixClient crypto", () => {
it("Bob uploads device keys", bobUploadsDeviceKeys);
it("handles failures to upload device keys", async () => {
// since device keys are uploaded asynchronously, there's not really much to do here other than fail the
// upload.
bobTestClient.httpBackend.when("POST", "/keys/upload").fail(0, new Error("bleh"));
await bobTestClient.httpBackend.flushAllExpected();
});
it("Ali downloads Bobs device keys", async () => {
await bobUploadsDeviceKeys();
await aliDownloadsKeys();
@@ -396,10 +380,7 @@ describe("MatrixClient crypto", () => {
const bobDeviceKeys = bobTestClient.deviceKeys!;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
expectAliQueryKeys(),
]);
await Promise.all([aliTestClient.client.downloadKeys([bobUserId]), expectAliQueryKeys()]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
// should get an empty list
expect(devices).toEqual([]);
@@ -409,26 +390,24 @@ describe("MatrixClient crypto", () => {
const eveUserId = "@eve:localhost";
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bvcxz',
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',
"ed25519:bvcxz": "pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q",
"curve25519:bvcxz": "7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ",
},
user_id: '@eve:localhost',
user_id: "@eve:localhost",
signatures: {
'@eve:localhost': {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
"@eve:localhost": {
"ed25519:bvcxz":
"CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG" + "0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg",
},
},
};
const bobKeys = {};
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
@@ -445,26 +424,24 @@ describe("MatrixClient crypto", () => {
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',
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',
"ed25519:bad_device": "e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0",
"curve25519:bad_device": "YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc",
},
user_id: '@bob:localhost',
user_id: "@bob:localhost",
signatures: {
'@bob:localhost': {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
"@bob:localhost": {
"ed25519:bad_device":
"fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A" + "me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ",
},
},
};
const bobKeys = {};
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
@@ -515,26 +492,25 @@ describe("MatrixClient crypto", () => {
next_batch: "x",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
},
},
},
};
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) {
const onEvent = function (event: MatrixEvent) {
logger.log(bobUserId + " received event", event);
resolve(event);
};
@@ -558,11 +534,10 @@ describe("MatrixClient crypto", () => {
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({});
});
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]);
});
@@ -588,9 +563,7 @@ describe("MatrixClient crypto", () => {
await firstSync(bobTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {},
);
bobTestClient.httpBackend.when("POST", "/keys/query").respond(200, {});
await bobRecvMessage();
await bobEnablesEncryption();
const ciphertext = await bobSendsReplyMessage();
@@ -605,28 +578,28 @@ describe("MatrixClient crypto", () => {
await aliTestClient.start();
await firstSync(aliTestClient);
const syncData = {
next_batch: '2',
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',
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.httpBackend.when("GET", "/sync").respond(200, syncData);
await aliTestClient.httpBackend.flush("/sync", 1);
aliTestClient.expectKeyQuery({
device_keys: {
[bobUserId]: {},
@@ -649,7 +622,7 @@ describe("MatrixClient crypto", () => {
// enqueue expectations:
// * Sync with empty one_time_keys => upload keys
logger.log(aliTestClient + ': starting');
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" });
@@ -659,24 +632,61 @@ describe("MatrixClient crypto", () => {
// 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 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();
});
it("Checks for outgoing room key requests for a given event's session", async () => {
const eventA0 = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: "m.megolm.v1.aes-sha2",
session_id: "sessionid",
sender_key: "senderkey",
},
});
const eventA1 = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: "m.megolm.v1.aes-sha2",
session_id: "sessionid",
sender_key: "senderkey",
},
});
const eventB = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: "m.megolm.v1.aes-sha2",
session_id: "othersessionid",
sender_key: "senderkey",
},
});
const nonEncryptedEvent = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {},
});
aliTestClient.client.crypto?.onSyncCompleted({});
await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0);
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull();
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull();
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull();
});
});
+90
View File
@@ -0,0 +1,90 @@
/*
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 "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { createClient } from "../../src";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
describe("MatrixClient.initRustCrypto", () => {
it("should raise if userId or deviceId is unknown", async () => {
const unknownUserClient = createClient({
baseUrl: "http://test.server",
deviceId: "aliceDevice",
});
await expect(() => unknownUserClient.initRustCrypto()).rejects.toThrow("unknown userId");
const unknownDeviceClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:test",
});
await expect(() => unknownDeviceClient.initRustCrypto()).rejects.toThrow("unknown deviceId");
});
it("should create the indexed dbs", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto();
// should have two dbs now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
);
});
it("should ignore a second call", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
await matrixClient.initRustCrypto();
await matrixClient.initRustCrypto();
});
});
describe("MatrixClient.clearStores", () => {
it("should clear the indexeddbs", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
await matrixClient.initRustCrypto();
expect(await indexedDB.databases()).toHaveLength(2);
await matrixClient.stopClient();
await matrixClient.clearStores();
expect(await indexedDB.databases()).toHaveLength(0);
});
});
+380 -80
View File
@@ -22,11 +22,23 @@ import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Exten
import { TestClient } from "../TestClient";
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
import {
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
MatrixClient,
MatrixEvent,
NotificationCountType,
JoinRule,
MatrixError,
EventType,
IPushRules,
PushRuleKind,
TweakName,
ClientEvent,
RoomMemberEvent,
RoomEvent,
Room,
IRoomTimelineData,
} from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync";
import { SyncApiOptions, SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
@@ -40,10 +52,9 @@ describe("SlidingSyncSdk", () => {
const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getList = jest.fn();
s.getListParams = jest.fn();
s.getListData = jest.fn();
s.getRoomSubscriptions = jest.fn();
s.listLength = jest.fn();
s.modifyRoomSubscriptionInfo = jest.fn();
s.modifyRoomSubscriptions = jest.fn();
s.registerExtension = jest.fn();
@@ -67,7 +78,7 @@ describe("SlidingSyncSdk", () => {
event_id: "$" + eventIdCounter,
};
};
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
const mkOwnStateEvent = (evType: string, content: object, stateKey = ""): IStateEvent => {
eventIdCounter++;
return {
type: evType,
@@ -97,19 +108,20 @@ describe("SlidingSyncSdk", () => {
};
// assign client/httpBackend globals
const setupClient = async (testOpts?: Partial<IStoredClientOpts&{withCrypto: boolean}>) => {
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
testOpts = testOpts || {};
const syncOpts: SyncApiOptions = {};
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto();
testOpts.crypto = client!.crypto;
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
}
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
};
// tear down client/httpBackend globals
@@ -119,13 +131,13 @@ describe("SlidingSyncSdk", () => {
};
// find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension => {
const findExtension = (name: string): Extension<any, any> => {
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) {
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
if (calledExtension?.name() === name) {
return calledExtension;
}
}
@@ -170,6 +182,7 @@ describe("SlidingSyncSdk", () => {
const roomE = "!e_with_invite:localhost";
const roomF = "!f_calc_room_name:localhost";
const roomG = "!g_join_invite_counts:localhost";
const roomH = "!g_num_live:localhost";
const data: Record<string, MSC3575RoomData> = {
[roomA]: {
name: "A",
@@ -194,7 +207,6 @@ describe("SlidingSyncSdk", () => {
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
],
initial: true,
},
@@ -275,13 +287,27 @@ describe("SlidingSyncSdk", () => {
invited_count: 2,
initial: true,
},
[roomH]: {
name: "H",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
],
initial: true,
num_live: 1,
},
};
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; }
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);
@@ -291,7 +317,9 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
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);
@@ -301,36 +329,73 @@ describe("SlidingSyncSdk", () => {
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);
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);
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; }
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 live events", () => {
let seenLiveEvent = false;
const listener = (
ev: MatrixEvent,
room?: Room,
toStartOfTimeline?: boolean,
deleted?: boolean,
timelineData?: IRoomTimelineData,
) => {
if (timelineData?.liveEvent) {
assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
seenLiveEvent = true;
}
};
client!.on(RoomEvent.Timeline, listener);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]);
client!.off(RoomEvent.Timeline, listener);
const gotRoom = client!.getRoom(roomH);
expect(gotRoom).toBeDefined();
if (gotRoom == null) {
return;
}
expect(gotRoom.name).toEqual(data[roomH].name);
expect(gotRoom.getMyMembership()).toEqual("join");
// check the entire timeline is correct
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline);
expect(seenLiveEvent).toBe(true);
});
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; }
if (gotRoom == null) {
return;
}
expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
});
@@ -339,10 +404,10 @@ describe("SlidingSyncSdk", () => {
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);
if (gotRoom == null) {
return;
}
expect(gotRoom.name).toEqual(data[roomF].name);
});
describe("updating", () => {
@@ -355,7 +420,9 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
if (gotRoom == null) {
return;
}
const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
@@ -364,18 +431,20 @@ describe("SlidingSyncSdk", () => {
it("can update with a new required_state event", async () => {
let gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
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" }, ""),
],
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; }
if (gotRoom == null) {
return;
}
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
});
@@ -388,10 +457,10 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(1);
if (gotRoom == null) {
return;
}
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
});
it("can update with a new notification_count", async () => {
@@ -403,10 +472,10 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(1);
if (gotRoom == null) {
return;
}
expect(gotRoom.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
});
it("can update with a new joined_count", () => {
@@ -418,7 +487,9 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
if (gotRoom == null) {
return;
}
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
});
@@ -442,11 +513,20 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
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)),
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)
@@ -466,40 +546,54 @@ describe("SlidingSyncSdk", () => {
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: {} },
);
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"),
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"),
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: {} },
);
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",
}));
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();
});
@@ -541,7 +635,8 @@ describe("SlidingSyncSdk", () => {
});
describe("ExtensionE2EE", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient({
withCrypto: true,
@@ -551,18 +646,21 @@ describe("SlidingSyncSdk", () => {
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: {
@@ -572,6 +670,7 @@ describe("SlidingSyncSdk", () => {
});
// TODO: more assertions?
});
it("can update OTK counts", () => {
client!.crypto!.updateOneTimeKeyCount = jest.fn();
ext.onResponse({
@@ -588,6 +687,7 @@ describe("SlidingSyncSdk", () => {
});
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
});
it("can update fallback keys", () => {
ext.onResponse({
device_unused_fallback_key_types: ["signed_curve25519"],
@@ -599,8 +699,10 @@ describe("SlidingSyncSdk", () => {
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
});
});
describe("ExtensionAccountData", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
@@ -608,12 +710,14 @@ describe("SlidingSyncSdk", () => {
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 = {
@@ -633,6 +737,7 @@ describe("SlidingSyncSdk", () => {
expect(globalData).toBeDefined();
expect(globalData.getContent()).toEqual(globalContent);
});
it("processes rooms account data", async () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
@@ -643,7 +748,6 @@ describe("SlidingSyncSdk", () => {
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
initial: true,
});
@@ -667,6 +771,7 @@ describe("SlidingSyncSdk", () => {
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";
@@ -686,22 +791,25 @@ describe("SlidingSyncSdk", () => {
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,
}],
[PushRuleKind.RoomSpecific]: [
{
enabled: true,
default: true,
pattern: "monkey",
actions: [
{
set_tweak: TweakName.Sound,
value: "default",
},
],
rule_id: roomId,
},
],
},
};
let pushRule = client!.getRoomPushRule("global", roomId);
@@ -718,8 +826,10 @@ describe("SlidingSyncSdk", () => {
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
});
});
describe("ExtensionToDevice", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
@@ -727,12 +837,14 @@ describe("SlidingSyncSdk", () => {
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",
@@ -742,12 +854,14 @@ describe("SlidingSyncSdk", () => {
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 = {
@@ -770,6 +884,7 @@ describe("SlidingSyncSdk", () => {
});
expect(called).toBe(true);
});
it("can cancel key verification requests", async () => {
const seen: Record<string, boolean> = {};
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
@@ -809,4 +924,189 @@ describe("SlidingSyncSdk", () => {
});
});
});
describe("ExtensionTyping", () => {
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("typing");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes typing notifications", async () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
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 room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({
rooms: {
[roomId]: {
type: EventType.Typing,
content: {
user_ids: [selfUserId],
},
},
},
});
expect(room.getMember(selfUserId)?.typing).toEqual(true);
ext.onResponse({
rooms: {
[roomId]: {
type: EventType.Typing,
content: {
user_ids: [],
},
},
},
});
expect(room.getMember(selfUserId)?.typing).toEqual(false);
});
it("gracefully handles missing rooms and members when typing", async () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
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 room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({
rooms: {
[roomId]: {
type: EventType.Typing,
content: {
user_ids: ["@someone:else"],
},
},
},
});
expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({
rooms: {
"!something:else": {
type: EventType.Typing,
content: {
user_ids: [selfUserId],
},
},
},
});
expect(room.getMember(selfUserId)?.typing).toEqual(false);
});
});
describe("ExtensionReceipts", () => {
let ext: Extension<any, any>;
const generateReceiptResponse = (
userId: string,
roomId: string,
eventId: string,
recType: string,
ts: number,
) => {
return {
rooms: {
[roomId]: {
type: EventType.Receipt,
content: {
[eventId]: {
[recType]: {
[userId]: {
ts: ts,
},
},
},
},
},
},
};
};
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("receipts");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes receipts", async () => {
const roomId = "!room:id";
const alice = "@alice:alice";
const lastEvent = mkOwnEvent(EventType.RoomMessage, { body: "hello" });
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with receipts",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
{
type: EventType.RoomMember,
state_key: alice,
content: { membership: "join" },
sender: alice,
origin_server_ts: Date.now(),
event_id: "$alice",
},
lastEvent,
],
initial: true,
});
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567));
const receipt = room.getReadReceiptForUserId(alice);
expect(receipt).toBeDefined();
expect(receipt?.eventId).toEqual(lastEvent.event_id);
expect(receipt?.data.ts).toEqual(1234567);
expect(receipt?.data.thread_id).toBeFalsy();
});
it("gracefully handles missing rooms when receiving receipts", async () => {
const roomId = "!room:id";
const alice = "@alice:alice";
const eventId = "$something";
ext.onResponse(generateReceiptResponse(alice, roomId, eventId, "m.read", 1234567));
// we expect it not to crash
});
});
});
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from '../src/logger';
import { logger } from "../src/logger";
// 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');
global.Olm = require("@matrix-org/olm");
logger.log("loaded libolm");
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available");
}
+100
View File
@@ -0,0 +1,100 @@
/*
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 no-console */
class JestSlowTestReporter {
constructor(globalConfig, options) {
this._globalConfig = globalConfig;
this._options = options;
this._slowTests = [];
this._slowTestSuites = [];
}
onRunComplete() {
const displayResult = (result, isTestSuite) => {
if (!isTestSuite) console.log();
result.sort((a, b) => b.duration - a.duration);
const rootPathRegex = new RegExp(`^${process.cwd()}`);
const slowestTests = result.slice(0, this._options.numTests || 10);
const slowTestTime = this._slowTestTime(slowestTests);
const allTestTime = this._allTestTime(result);
const percentTime = (slowTestTime / allTestTime) * 100;
if (isTestSuite) {
console.log(
`Top ${slowestTests.length} slowest test suites (${slowTestTime / 1000} seconds,` +
` ${percentTime.toFixed(1)}% of total time):`,
);
} else {
console.log(
`Top ${slowestTests.length} slowest tests (${slowTestTime / 1000} seconds,` +
` ${percentTime.toFixed(1)}% of total time):`,
);
}
for (let i = 0; i < slowestTests.length; i++) {
const duration = slowestTests[i].duration;
const filePath = slowestTests[i].filePath.replace(rootPathRegex, ".");
if (isTestSuite) {
console.log(` ${duration / 1000} seconds ${filePath}`);
} else {
const fullName = slowestTests[i].fullName;
console.log(` ${fullName}`);
console.log(` ${duration / 1000} seconds ${filePath}`);
}
}
console.log();
};
displayResult(this._slowTests);
displayResult(this._slowTestSuites, true);
}
onTestResult(test, testResult) {
this._slowTestSuites.push({
duration: testResult.perfStats.runtime,
filePath: testResult.testFilePath,
});
for (let i = 0; i < testResult.testResults.length; i++) {
this._slowTests.push({
duration: testResult.testResults[i].duration,
fullName: testResult.testResults[i].fullName,
filePath: testResult.testFilePath,
});
}
}
_slowTestTime(slowestTests) {
let slowTestTime = 0;
for (let i = 0; i < slowestTests.length; i++) {
slowTestTime += slowestTests[i].duration;
}
return slowTestTime;
}
_allTestTime(result) {
let allTestTime = 0;
for (let i = 0; i < result.length; i++) {
allTestTime += result[i].duration;
}
return allTestTime;
}
}
module.exports = JestSlowTestReporter;
+12 -21
View File
@@ -17,10 +17,7 @@ limitations under the License.
import { MatrixEvent } from "../../src";
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType } from "../../src/@types/location";
import {
makeBeaconContent,
makeBeaconInfoContent,
} from "../../src/content-helpers";
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
type InfoContentProps = {
timeout: number;
@@ -44,13 +41,7 @@ export const makeBeaconInfoEvent = (
contentProps: Partial<InfoContentProps> = {},
eventId?: string,
): MatrixEvent => {
const {
timeout,
isLive,
description,
assetType,
timestamp,
} = {
const { timeout, isLive, description, assetType, timestamp } = {
...DEFAULT_INFO_CONTENT_PROPS,
...contentProps,
};
@@ -77,9 +68,9 @@ type ContentProps = {
description?: string;
};
const DEFAULT_CONTENT_PROPS: ContentProps = {
uri: 'geo:-36.24484561954707,175.46884959563613;u=10',
uri: "geo:-36.24484561954707,175.46884959563613;u=10",
timestamp: 123,
beaconInfoId: '$123',
beaconInfoId: "$123",
};
/**
@@ -87,10 +78,7 @@ const DEFAULT_CONTENT_PROPS: ContentProps = {
* all required properties are mocked
* override with contentProps
*/
export const makeBeaconEvent = (
sender: string,
contentProps: Partial<ContentProps> = {},
): MatrixEvent => {
export const makeBeaconEvent = (sender: string, contentProps: Partial<ContentProps> = {}): MatrixEvent => {
const { uri, timestamp, beaconInfoId, description } = {
...DEFAULT_CONTENT_PROPS,
...contentProps,
@@ -107,10 +95,13 @@ export const makeBeaconEvent = (
* Create a mock geolocation position
* defaults all required properties
*/
export const makeGeolocationPosition = (
{ timestamp, coords }:
{ timestamp?: number, coords: Partial<GeolocationCoordinates> },
): GeolocationPosition => ({
export const makeGeolocationPosition = ({
timestamp,
coords,
}: {
timestamp?: number;
coords: Partial<GeolocationCoordinates>;
}): GeolocationPosition => ({
timestamp: timestamp ?? 1647256791840,
coords: {
accuracy: 1,
+2 -3
View File
@@ -58,11 +58,11 @@ export const getMockClientWithEventEmitter = (
});
* ```
*/
export const mockClientMethodsUser = (userId = '@alice: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'),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
@@ -91,4 +91,3 @@ export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixC
getCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
});
+1 -1
View File
@@ -22,7 +22,7 @@ limitations under the License.
// and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
export function flushPromises() {
return new Promise(r => {
return new Promise((r) => {
realSetTimeout(r, 1);
});
}
+97 -74
View File
@@ -2,9 +2,9 @@
import EventEmitter from "events";
// load olm before the sdk if possible
import '../olm-loader';
import "../olm-loader";
import { logger } from '../../src/logger';
import { logger } from "../../src/logger";
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import { SyncState } from "../../src/sync";
@@ -13,9 +13,9 @@ import { eventMapperFor } from "../../src/event-mapper";
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
* @param client - The client
* @param count - Number of syncs to wait for (default 1)
* @returns Promise which resolves once the client has emitted a SYNCING event
*/
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
if (count <= 0) {
@@ -41,20 +41,21 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
/**
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
* @param constr - The class constructor (used with 'new')
* @param name - The name of the class
* @returns An instantiated object with spied methods/properties.
*/
export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
// @ts-ignore
const result = new HelperConstr();
result.toString = function() {
result.toString = function () {
return "mock" + (name ? " of " + name : "");
};
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
for (const key of Object.getOwnPropertyNames(constr.prototype)) {
// eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
@@ -84,15 +85,15 @@ interface IEventOpts {
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object} a JSON object representing this event.
* @param opts - Values for the event.
* @param opts.type - The event.type
* @param opts.room - The event.room_id
* @param opts.sender - The event.sender
* @param opts.skey - Optional. The state key (auto inserts empty string)
* @param opts.content - The event.content
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns 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): Partial<IEvent>;
@@ -114,15 +115,17 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
} else if ([
EventType.RoomName,
EventType.RoomTopic,
EventType.RoomCreate,
EventType.RoomJoinRules,
EventType.RoomPowerLevels,
EventType.RoomTopic,
"com.example.state",
].includes(opts.type)) {
} else if (
[
EventType.RoomName,
EventType.RoomTopic,
EventType.RoomCreate,
EventType.RoomJoinRules,
EventType.RoomPowerLevels,
EventType.RoomTopic,
"com.example.state",
].includes(opts.type)
) {
event.state_key = "";
}
@@ -160,8 +163,8 @@ interface IPresenceOpts {
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
* @param opts - Values for the presence.
* @returns The event
*/
export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent;
export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial<IEvent>;
@@ -193,16 +196,16 @@ interface IMembershipOpts {
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* @param opts - Values for the membership.
* @param opts.room - The room ID for the event.
* @param opts.mship - The content.membership for the event.
* @param opts.sender - The sender user ID for the event.
* @param opts.skey - The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
* @param opts.name - The content.displayname for the event.
* @param opts.url - The content.avatar_url for the event.
* @param opts.event - True to make a MatrixEvent.
* @returns The event
*/
export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent;
export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial<IEvent>;
@@ -228,8 +231,8 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Parti
}
export function mkMembershipCustom<T>(
base: T & { membership: string, sender: string, content?: IContent },
): T & { type: EventType, sender: string, state_key: string, content: IContent } & GeneratedMetadata {
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,
@@ -250,13 +253,13 @@ export interface IMessageOpts {
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
* @param opts - Values for the message
* @param opts.room - The room ID for the event.
* @param opts.user - The user ID for the event.
* @param opts.msg - Optional. The content.body for the event.
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns The event
*/
export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
@@ -290,14 +293,14 @@ interface IReplyMessageOpts extends IMessageOpts {
/**
* Create a reply message.
*
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {MatrixEvent} opts.replyToMessage The replied message
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
* @param opts - Values for the message
* @param opts.room - The room ID for the event.
* @param opts.user - The user ID for the event.
* @param opts.msg - Optional. The content.body for the event.
* @param opts.replyToMessage - The replied message
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns The event
*/
export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
@@ -315,7 +318,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()!,
},
},
},
@@ -329,10 +332,8 @@ export function mkReplyMessage(
/**
* A mock implementation of webstorage
*
* @constructor
*/
export class MockStorageApi {
export class MockStorageApi implements Storage {
private data: Record<string, any> = {};
public get length() {
@@ -354,33 +355,43 @@ export class MockStorageApi {
public removeItem(k: string): void {
delete this.data[k];
}
public clear(): void {
this.data = {};
}
}
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
* @returns promise which resolves (to `event`) when the event has been decrypted
*/
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
export async function awaitDecryption(
event: MatrixEvent,
{ waitOnDecryptionFailure = false } = {},
): Promise<MatrixEvent> {
// An event is not always decrypted ahead of time
// getClearContent is a good signal to know whether an event has been decrypted
// already
if (event.getClearContent() !== null) {
return event;
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
logger.log(`${Date.now()} event ${event.getId()} got decryption error; waiting`);
} else {
return event;
}
} else {
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
logger.log(`${Date.now()} event ${event.getId()} is not yet decrypted; waiting`);
}
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
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",
@@ -392,3 +403,15 @@ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
pushkey: "pushpush",
...extra,
});
/**
* a list of the supported crypto implementations, each with a callback to initialise that implementation
* for the given client
*/
export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
export type InitCrypto = (_: MatrixClient) => Promise<void>;
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
if (global.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
}
+40 -31
View File
@@ -21,18 +21,25 @@ 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,
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"];
@@ -50,12 +57,17 @@ type MakeThreadEventsProps = {
};
export const makeThreadEvents = ({
roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId,
}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => {
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(),
msg: "root event message " + Math.random(),
ts,
event: true,
});
@@ -67,16 +79,18 @@ export const makeThreadEvents = ({
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,
}));
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({
@@ -108,7 +122,7 @@ export const mkThread = ({
participantUserIds,
length = 2,
ts = 1,
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => {
}: MakeThreadProps): { thread: Thread; rootEvent: MatrixEvent; events: MatrixEvent[] } => {
const { rootEvent, events } = makeThreadEvents({
roomId: room.roomId,
authorId,
@@ -120,15 +134,10 @@ export const mkThread = ({
expect(rootEvent).toBeTruthy();
for (const evt of events) {
room?.reEmitter.reEmit(evt, [
MatrixEventEvent.BeforeRedaction,
]);
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 };
};
+179 -80
View File
@@ -26,6 +26,7 @@ import {
MatrixClient,
MatrixEvent,
Room,
RoomMember,
RoomState,
RoomStateEvent,
RoomStateEventHandlerMap,
@@ -33,14 +34,14 @@ import {
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 { CallEvent, CallEventHandlerMap, CallState, 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 = (
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" +
@@ -77,24 +78,40 @@ export const DUMMY_SDP = (
"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"
);
"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";
export const FAKE_ROOM_ID = "!fake:test.dummy";
export const FAKE_CONF_ID = "fakegroupcallid";
export const FAKE_USER_ID_1 = "@alice:test.dummy";
export const FAKE_DEVICE_ID_1 = "@AAAAAA";
export const FAKE_SESSION_ID_1 = "alice1";
export const FAKE_USER_ID_2 = "@bob:test.dummy";
export const FAKE_DEVICE_ID_2 = "@BBBBBB";
export const FAKE_SESSION_ID_2 = "bob1";
export const FAKE_USER_ID_3 = "@charlie:test.dummy";
class MockMediaStreamAudioSourceNode {
public connect() {}
}
class MockAnalyser {
public getFloatFrequencyData() { return 0.0; }
public getFloatFrequencyData() {
return 0.0;
}
}
export class MockAudioContext {
constructor() {}
public createAnalyser() { return new MockAnalyser(); }
public createMediaStreamSource() { return new MockMediaStreamAudioSourceNode(); }
public createAnalyser() {
return new MockAnalyser();
}
public createMediaStreamSource() {
return new MockMediaStreamAudioSourceNode();
}
public close() {}
}
@@ -103,12 +120,14 @@ export class MockRTCPeerConnection {
private negotiationNeededListener?: () => void;
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
public iceConnectionStateChangeListener?: () => void;
public onTrackListener?: (e: RTCTrackEvent) => void;
public needsNegotiation = false;
public readyToNegotiate: Promise<void>;
private onReadyToNegotiate?: () => void;
public localDescription: RTCSessionDescription;
public signalingState: RTCSignalingState = "stable";
public iceConnectionState: RTCIceConnectionState = "connected";
public transceivers: MockRTCRtpTransceiver[] = [];
public static triggerAllNegotiations(): void {
@@ -118,7 +137,7 @@ export class MockRTCPeerConnection {
}
public static hasAnyPendingNegotiations(): boolean {
return this.instances.some(i => i.needsNegotiation);
return this.instances.some((i) => i.needsNegotiation);
}
public static resetInstances() {
@@ -128,11 +147,11 @@ export class MockRTCPeerConnection {
constructor() {
this.localDescription = {
sdp: DUMMY_SDP,
type: 'offer',
toJSON: function() { },
type: "offer",
toJSON: function () {},
};
this.readyToNegotiate = new Promise<void>(resolve => {
this.readyToNegotiate = new Promise<void>((resolve) => {
this.onReadyToNegotiate = resolve;
});
@@ -140,24 +159,28 @@ export class MockRTCPeerConnection {
}
public addEventListener(type: string, listener: () => void) {
if (type === 'negotiationneeded') {
if (type === "negotiationneeded") {
this.negotiationNeededListener = listener;
} else if (type == 'icecandidate') {
} else if (type == "icecandidate") {
this.iceCandidateListener = listener;
} else if (type == 'track') {
} else if (type === "iceconnectionstatechange") {
this.iceConnectionStateChangeListener = listener;
} else if (type == "track") {
this.onTrackListener = listener;
}
}
public createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
public createDataChannel(label: string, opts: RTCDataChannelInit) {
return { label, ...opts };
}
public createOffer() {
return Promise.resolve({
type: 'offer',
type: "offer",
sdp: DUMMY_SDP,
});
}
public createAnswer() {
return Promise.resolve({
type: 'answer',
type: "answer",
sdp: DUMMY_SDP,
});
}
@@ -167,8 +190,10 @@ export class MockRTCPeerConnection {
public setLocalDescription() {
return Promise.resolve();
}
public close() { }
public getStats() { return []; }
public close() {}
public getStats() {
return [];
}
public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
this.needsNegotiation = true;
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
@@ -193,9 +218,11 @@ export class MockRTCPeerConnection {
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
}
public getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; }
public getTransceivers(): MockRTCRtpTransceiver[] {
return this.transceivers;
}
public getSenders(): MockRTCRtpSender[] {
return this.transceivers.map(t => t.sender as unknown as MockRTCRtpSender);
return this.transceivers.map((t) => t.sender as unknown as MockRTCRtpSender);
}
public doNegotiation() {
@@ -207,13 +234,15 @@ export class MockRTCPeerConnection {
}
export class MockRTCRtpSender {
constructor(public track: MockMediaStreamTrack) { }
constructor(public track: MockMediaStreamTrack) {}
public replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
public replaceTrack(track: MockMediaStreamTrack) {
this.track = track;
}
}
export class MockRTCRtpReceiver {
constructor(public track: MockMediaStreamTrack) { }
constructor(public track: MockMediaStreamTrack) {}
}
export class MockRTCRtpTransceiver {
@@ -230,7 +259,7 @@ export class MockRTCRtpTransceiver {
}
export class MockMediaStreamTrack {
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
public stop = jest.fn<void, []>();
@@ -238,7 +267,9 @@ export class MockMediaStreamTrack {
public isStopped = false;
public settings?: MediaTrackSettings;
public getSettings(): MediaTrackSettings { return this.settings!; }
public getSettings(): MediaTrackSettings {
return this.settings!;
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
@@ -257,16 +288,15 @@ export class MockMediaStreamTrack {
});
}
public typed(): MediaStreamTrack { return this as unknown as MediaStreamTrack; }
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[] = [],
) {}
constructor(public id: string, private tracks: MockMediaStreamTrack[] = []) {}
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
@@ -277,9 +307,15 @@ export class MockMediaStream {
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 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]);
}
@@ -292,7 +328,9 @@ export class MockMediaStream {
this.tracks.push(track);
this.dispatchEvent("addtrack");
}
public removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
public removeTrack(track: MockMediaStreamTrack) {
this.tracks.splice(this.tracks.indexOf(track), 1);
}
public clone(): MediaStream {
return new MockMediaStream(this.id + ".clone", this.tracks).typed();
@@ -309,11 +347,11 @@ export class MockMediaStream {
}
export class MockMediaDeviceInfo {
constructor(
public kind: "audioinput" | "videoinput" | "audiooutput",
) { }
constructor(public kind: "audioinput" | "videoinput" | "audiooutput") {}
public typed(): MediaDeviceInfo { return this as unknown as MediaDeviceInfo; }
public typed(): MediaDeviceInfo {
return this as unknown as MediaDeviceInfo;
}
}
export class MockMediaHandler {
@@ -343,28 +381,38 @@ export class MockMediaHandler {
public stopScreensharingStream(stream: MockMediaStream) {
stream.isStopped = true;
}
public hasAudioDevice() { return true; }
public hasVideoDevice() { return true; }
public hasAudioDevice() {
return true;
}
public hasVideoDevice() {
return true;
}
public stopAllStreams() {}
public typed(): MediaHandler { return this as unknown as MediaHandler; }
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 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 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 getDisplayMedia = jest
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
public typed(): MediaDevices { return this as unknown as MediaDevices; }
public typed(): MediaDevices {
return this as unknown as MediaDevices;
}
}
type EmittedEvents = CallEventHandlerEvent | CallEvent | ClientEvent | RoomStateEvent | GroupCallEventHandlerEvent;
@@ -389,21 +437,33 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
calls: new Map<string, MatrixCall>(),
};
public sendStateEvent = jest.fn<Promise<ISendEventResponse>, [
roomId: string, eventType: EventType, content: any, statekey: string,
]>();
public sendToDevice = jest.fn<Promise<{}>, [
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
txnId?: string,
]>();
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 getMediaHandler(): MediaHandler {
return this.mediaHandler.typed();
}
public getUserId(): string { return this.userId; }
public getUserId(): string {
return this.userId;
}
public getDeviceId(): string { return this.deviceId; }
public getSessionId(): string { return this.sessionId; }
public getDeviceId(): string {
return this.deviceId;
}
public getSessionId(): string {
return this.sessionId;
}
public getTurnServers = () => [];
public isFallbackICEServerAllowed = () => false;
@@ -416,23 +476,58 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();
public typed(): MatrixClient { return this as unknown as MatrixClient; }
public supportsExperimentalThreads(): boolean {
return true;
}
public async decryptEventIfNeeded(): Promise<void> {}
public typed(): MatrixClient {
return this as unknown as MatrixClient;
}
public emitRoomState(event: MatrixEvent, state: RoomState): void {
this.emit(
RoomStateEvent.Events,
event,
state,
null,
);
this.emit(RoomStateEvent.Events, event, state, null);
}
}
export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
constructor(public roomId: string, public groupCallId?: string) {
super();
}
public state = CallState.Ringing;
public opponentUserId = FAKE_USER_ID_1;
public opponentDeviceId = FAKE_DEVICE_ID_1;
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
stream: new MockMediaStream("stream"),
};
public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
public hangup = jest.fn<void, []>();
public sendMetadataUpdate = jest.fn<void, []>();
public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember;
}
public getOpponentDeviceId(): string | undefined {
return this.opponentDeviceId;
}
public typed(): MatrixCall {
return this as unknown as MatrixCall;
}
}
export class MockCallFeed {
constructor(
public userId: string,
public stream: MockMediaStream,
) {}
constructor(public userId: string, public deviceId: string | undefined, public stream: MockMediaStream) {}
public measureVolumeActivity(val: boolean) {}
public dispose() {}
@@ -479,10 +574,14 @@ export function installWebRTCMocks() {
};
}
export function makeMockGroupCallStateEvent(roomId: string, groupCallId: string, content: IContent = {
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
}): MatrixEvent {
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),
+7 -9
View File
@@ -27,16 +27,14 @@ class EventSource extends EventEmitter {
}
doAnError() {
this.emit('error');
this.emit("error");
}
}
class EventTarget extends EventEmitter {
class EventTarget extends EventEmitter {}
}
describe("ReEmitter", function() {
it("Re-Emits events with the same args", function() {
describe("ReEmitter", function () {
it("Re-Emits events with the same args", function () {
const src = new EventSource();
const tgt = new EventTarget();
@@ -53,18 +51,18 @@ describe("ReEmitter", function() {
expect(handler).toHaveBeenCalledWith("foo", "bar", src);
});
it("Doesn't throw if no handler for 'error' event", function() {
it("Doesn't throw if no handler for 'error' event", function () {
const src = new EventSource();
const tgt = new EventTarget();
const reEmitter = new ReEmitter(tgt);
reEmitter.reEmit(src, ['error']);
reEmitter.reEmit(src, ["error"]);
// without the workaround in ReEmitter, this would throw
src.doAnError();
const handler = jest.fn();
tgt.on('error', handler);
tgt.on("error", handler);
src.doAnError();
+106
View File
@@ -0,0 +1,106 @@
import { ConnectionError } from "../../src/http-api/errors";
import { ClientEvent, MatrixClient, Store } from "../../src/client";
import { ToDeviceMessageQueue } from "../../src/ToDeviceMessageQueue";
import { getMockClientWithEventEmitter } from "../test-utils/client";
import { StubStore } from "../../src/store/stub";
import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { SyncState } from "../../src/sync";
describe("onResumedSync", () => {
let batch: IndexedToDeviceBatch | null;
let shouldFailSendToDevice: Boolean;
let onSendToDeviceFailure: () => void;
let onSendToDeviceSuccess: () => void;
let resumeSync: (newState: SyncState, oldState: SyncState) => void;
let store: Store;
let mockClient: MatrixClient;
let queue: ToDeviceMessageQueue;
beforeEach(() => {
batch = {
id: 0,
txnId: "123",
eventType: "m.dummy",
batch: [],
};
shouldFailSendToDevice = true;
onSendToDeviceFailure = () => {};
onSendToDeviceSuccess = () => {};
resumeSync = (newState, oldState) => {
shouldFailSendToDevice = false;
mockClient.emit(ClientEvent.Sync, newState, oldState);
};
store = new StubStore();
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
return batch;
});
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
batch = null;
});
mockClient = getMockClientWithEventEmitter({});
mockClient.store = store;
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
if (shouldFailSendToDevice) {
await Promise.reject(new ConnectionError("")).finally(() => {
setTimeout(onSendToDeviceFailure, 0);
});
} else {
await Promise.resolve({}).finally(() => {
setTimeout(onSendToDeviceSuccess, 0);
});
}
});
queue = new ToDeviceMessageQueue(mockClient);
});
it("resends queue after connectivity restored", (done) => {
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
resumeSync(SyncState.Syncing, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(2);
};
onSendToDeviceSuccess = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3);
expect(store.removeToDeviceBatch).toHaveBeenCalled();
done();
};
queue.start();
});
it("does not resend queue if client sync still catching up", (done) => {
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
resumeSync(SyncState.Catchup, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
done();
};
queue.start();
});
it("does not resend queue if connectivity restored after queue stopped", (done) => {
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
queue.stop();
resumeSync(SyncState.Syncing, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
done();
};
queue.start();
});
});
+372 -325
View File
@@ -19,44 +19,56 @@ import MockHttpBackend from "matrix-mock-request";
import { AutoDiscovery } from "../../src/autodiscovery";
describe("AutoDiscovery", function() {
describe("AutoDiscovery", function () {
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() {
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");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(/* no args */).then(
() => {
throw new Error("Expected a failure, not success with no args");
},
() => {
return true;
},
),
AutoDiscovery.findClientConfig("").then(() => {
throw new Error("Expected a failure, not success with an empty string");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig("").then(
() => {
throw new Error("Expected a failure, not success with an empty string");
},
() => {
return true;
},
),
AutoDiscovery.findClientConfig(null as any).then(() => {
throw new Error("Expected a failure, not success with null");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(null as any).then(
() => {
throw new Error("Expected a failure, not success with null");
},
() => {
return true;
},
),
AutoDiscovery.findClientConfig(true as any).then(() => {
throw new Error("Expected a failure, not success with a non-string");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(true as any).then(
() => {
throw new Error("Expected a failure, not success with a non-string");
},
() => {
return true;
},
),
]);
});
it("should return PROMPT when .well-known 404s", 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([
@@ -80,7 +92,7 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known returns a 500 error", 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([
@@ -104,7 +116,7 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known returns a 400 error", 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([
@@ -128,7 +140,7 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known returns an empty body", 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([
@@ -169,9 +181,7 @@ describe("AutoDiscovery", function() {
};
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then(
expect(expected).toEqual,
),
AutoDiscovery.findClientConfig("example.org").then(expect(expected).toEqual),
]);
});
@@ -257,106 +267,117 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 404)", 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": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
},
);
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": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
},
);
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"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
},
);
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").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
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": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
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"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
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.path).toEqual("https://example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend
.when("GET", "/_matrix/client/versions")
.check((req) => {
expect(req.path).toEqual("https://example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
@@ -383,14 +404,16 @@ describe("AutoDiscovery", function() {
]);
});
it("should return SUCCESS with the right homeserver URL", function() {
it("should return SUCCESS with the right homeserver URL", function () {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend
.when("GET", "/_matrix/client/versions")
.check((req) => {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
@@ -418,185 +441,206 @@ describe("AutoDiscovery", function() {
]);
});
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (missing base_url)", 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.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
not_base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
},
);
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.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
},
);
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.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
};
expect(conf).toEqual(expected);
}),
]);
},
);
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.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
};
expect(conf).toEqual(expected);
}),
]);
},
);
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.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
not_base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
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.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
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.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
};
expect(conf).toEqual(expected);
}),
]);
});
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.path)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
};
expect(conf).toEqual(expected);
}),
]);
});
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.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.path)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend
.when("GET", "/_matrix/client/versions")
.check((req) => {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
});
httpBackend
.when("GET", "/_matrix/identity/v2")
.check((req) => {
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
})
.respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
@@ -627,19 +671,22 @@ describe("AutoDiscovery", function() {
]);
});
it("should return SUCCESS and preserve non-standard keys from the " +
".well-known response", 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.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.path)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend
.when("GET", "/_matrix/client/versions")
.check((req) => {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
});
httpBackend
.when("GET", "/_matrix/identity/v2")
.check((req) => {
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
})
.respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
+108 -103
View File
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
import { M_TOPIC } from "../../src/@types/topic";
import {
@@ -25,24 +23,20 @@ import {
parseBeaconContent,
parseTopicContent,
} from "../../src/content-helpers";
import { REFERENCE_RELATION } from "../../src/@types/extensible_events";
describe('Beacon content helpers', () => {
describe('makeBeaconInfoContent()', () => {
describe("Beacon content helpers", () => {
describe("makeBeaconInfoContent()", () => {
const mockDateNow = 123456789;
beforeEach(() => {
jest.spyOn(global.Date, 'now').mockReturnValue(mockDateNow);
jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
});
afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore();
jest.spyOn(global.Date, "now").mockRestore();
});
it('create fully defined event content', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual({
description: 'nice beacon_info',
it("create fully defined event content", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
description: "nice beacon_info",
timeout: 1234,
live: true,
[M_TIMESTAMP.name]: mockDateNow,
@@ -52,78 +46,72 @@ describe('Beacon content helpers', () => {
});
});
it('defaults timestamp to current time', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: mockDateNow,
}));
it("defaults timestamp to current time", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual(
expect.objectContaining({
[M_TIMESTAMP.name]: mockDateNow,
}),
);
});
it('uses timestamp when provided', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
99999,
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: 99999,
}));
it("uses timestamp when provided", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin, 99999)).toEqual(
expect.objectContaining({
[M_TIMESTAMP.name]: 99999,
}),
);
});
it('defaults asset type to self when not set', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
// no assetType passed
)).toEqual(expect.objectContaining({
[M_ASSET.name]: {
type: LocationAssetType.Self,
},
}));
it("defaults asset type to self when not set", () => {
expect(
makeBeaconInfoContent(
1234,
true,
"nice beacon_info",
// no assetType passed
),
).toEqual(
expect.objectContaining({
[M_ASSET.name]: {
type: LocationAssetType.Self,
},
}),
);
});
});
describe('makeBeaconContent()', () => {
it('creates event content without description', () => {
expect(makeBeaconContent(
'geo:foo',
123,
'$1234',
// no description
)).toEqual({
describe("makeBeaconContent()", () => {
it("creates event content without description", () => {
expect(
makeBeaconContent(
"geo:foo",
123,
"$1234",
// no description
),
).toEqual({
[M_LOCATION.name]: {
description: undefined,
uri: 'geo:foo',
uri: "geo:foo",
},
[M_TIMESTAMP.name]: 123,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: '$1234',
event_id: "$1234",
},
});
});
it('creates event content with description', () => {
expect(makeBeaconContent(
'geo:foo',
123,
'$1234',
'test description',
)).toEqual({
it("creates event content with description", () => {
expect(makeBeaconContent("geo:foo", 123, "$1234", "test description")).toEqual({
[M_LOCATION.name]: {
description: 'test description',
uri: 'geo:foo',
description: "test description",
uri: "geo:foo",
},
[M_TIMESTAMP.name]: 123,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: '$1234',
event_id: "$1234",
},
});
});
@@ -190,64 +178,81 @@ describe('Beacon content helpers', () => {
});
});
describe('Topic content helpers', () => {
describe('makeTopicContent()', () => {
it('creates fully defined event content without html', () => {
describe("Topic content helpers", () => {
describe("makeTopicContent()", () => {
it("creates fully defined event content without html", () => {
expect(makeTopicContent("pizza")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}],
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
],
});
});
it('creates fully defined event content with html', () => {
it("creates fully defined event content with html", () => {
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}, {
body: "<b>pizza</b>",
mimetype: "text/html",
}],
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
],
});
});
});
describe('parseTopicContent()', () => {
it('parses event content with plain text topic without mimetype', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
}],
})).toEqual({
describe("parseTopicContent()", () => {
it("parses event content with plain text topic without mimetype", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
},
],
}),
).toEqual({
text: "pizza",
});
});
it('parses event content with plain text topic', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}],
})).toEqual({
it("parses event content with plain text topic", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
],
}),
).toEqual({
text: "pizza",
});
});
it('parses event content with html topic', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "<b>pizza</b>",
mimetype: "text/html",
}],
})).toEqual({
it("parses event content with html topic", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
],
}),
).toEqual({
text: "pizza",
html: "<b>pizza</b>",
});
+32 -42
View File
@@ -16,60 +16,50 @@ limitations under the License.
import { getHttpUriForMxc } from "../../src/content-repo";
describe("ContentRepo", function() {
describe("ContentRepo", function () {
const baseUrl = "https://my.home.server";
describe("getHttpUriForMxc", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
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);
expect(getHttpUriForMxc(baseUrl, httpUrl, undefined, undefined, undefined, true)).toEqual(httpUrl);
});
it("should return the empty string HTTP URLs by default", function() {
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 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 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 return the empty string for null input", function () {
expect(getHttpUriForMxc(null as any, "")).toEqual("");
});
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 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 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",
);
});
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",
);
});
});
});
+401 -295
View File
File diff suppressed because it is too large Load Diff
+120 -138
View File
@@ -14,28 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../olm-loader';
import {
CrossSigningInfo,
createCryptoStoreCacheCallbacks,
} from '../../../src/crypto/CrossSigning';
import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
import "../../olm-loader";
import { CrossSigningInfo, createCryptoStoreCacheCallbacks } from "../../../src/crypto/CrossSigning";
import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
import "fake-indexeddb/auto";
import "jest-localstorage-mock";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { logger } from '../../../src/logger';
import { logger } from "../../../src/logger";
const userId = "@alice:example.com";
// Private key for tests only
const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, 0x05,
0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const types = [
@@ -50,13 +43,13 @@ badKey[0] ^= 1;
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function() {
describe("CrossSigningInfo.getCrossSigningKey", function () {
if (!global.Olm) {
logger.warn('Not running megolm backup unit tests: libolm not present');
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return global.Olm.init();
});
@@ -65,13 +58,12 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
await expect(info.getCrossSigningKey("master")).rejects.toThrow();
});
it.each(types)("should throw if the callback returns falsey",
async ({ type, shouldCache }) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: async () => false as unknown as Uint8Array,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
it.each(types)("should throw if the callback returns falsey", async ({ type, shouldCache }) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: async () => false as unknown as Uint8Array,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
});
it("should throw if the expected key doesn't come back", async () => {
const info = new CrossSigningInfo(userId, {
@@ -96,63 +88,8 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
}
});
it.each(types)("should request a key from the cache callback (if set)" +
" and does not call app if one is found" +
" %o",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
if (shouldCache) {
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
}
});
it.each(types)("should store a key with the cache callback (if set)",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
if (shouldCache) {
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
}
});
it.each(types)("does not store a bad key to the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
it.each(types)("does not store a value to the cache if it came from the cache",
it.each(types)(
"should request a key from the cache callback (if set)" + " and does not call app if one is found" + " %o",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
@@ -162,56 +99,98 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
new Error("Tried to store a value from cache"),
);
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
if (shouldCache) {
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
}
},
);
it.each(types)("should store a key with the cache callback (if set)", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
if (shouldCache) {
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
}
});
it.each(types)("does not store a bad key to the cache", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
it.each(types)("does not store a value to the cache if it came from the cache", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(new Error("Tried to store a value from cache"));
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
it.each(types)(
"requests a key from the cache callback (if set) and then calls app" + " if one is not found",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
" if one is not found", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
},
);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
it.each(types)(
"requests a key from the cache callback (if set) and then" + " calls app if that key doesn't match",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
it.each(types)("requests a key from the cache callback (if set) and then" +
" calls app if that key doesn't match", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
},
);
});
/*
@@ -219,20 +198,21 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
* it's not possible to get one in normal execution unless you hack as we do here.
*/
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;
}],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
let store;
["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;
},
],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function (name, dbFactory) {
let store: IndexedDBCryptoStore;
beforeAll(() => {
store = dbFactory();
@@ -245,8 +225,10 @@ describe.each([
it("should cache data to the store and retrieve it", async () => {
await store.startup();
const olmDevice = new OlmDevice(store);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
createCryptoStoreCacheCallbacks(store, olmDevice);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } = createCryptoStoreCacheCallbacks(
store,
olmDevice,
);
await storeCrossSigningKeyCache!("self_signing", testKey);
// If we've not saved anything, don't expect anything
+84 -91
View File
@@ -22,33 +22,29 @@ import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store
import { DeviceList } from "../../../src/crypto/DeviceList";
import { IDownloadKeyResult, MatrixClient } from "../../../src";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { CryptoStore } from "../../../src/crypto/store/base";
const signedDeviceList: IDownloadKeyResult = {
"failures": {},
"device_keys": {
failures: {},
device_keys: {
"@test1:sw1v.org": {
"HGKAWHRVJQ": {
"signatures": {
HGKAWHRVJQ: {
signatures: {
"@test1:sw1v.org": {
"ed25519:HGKAWHRVJQ":
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
},
},
"user_id": "@test1:sw1v.org",
"keys": {
"ed25519:HGKAWHRVJQ":
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
"curve25519:HGKAWHRVJQ":
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
user_id: "@test1:sw1v.org",
keys: {
"ed25519:HGKAWHRVJQ": "0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
"curve25519:HGKAWHRVJQ": "mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "HGKAWHRVJQ",
"unsigned": {
"device_display_name": "",
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "HGKAWHRVJQ",
unsigned: {
device_display_name: "",
},
},
},
@@ -56,50 +52,45 @@ const signedDeviceList: IDownloadKeyResult = {
};
const signedDeviceList2: IDownloadKeyResult = {
"failures": {},
"device_keys": {
failures: {},
device_keys: {
"@test2:sw1v.org": {
"QJVRHWAKGH": {
"signatures": {
QJVRHWAKGH: {
signatures: {
"@test2:sw1v.org": {
"ed25519:QJVRHWAKGH":
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
},
},
"user_id": "@test2:sw1v.org",
"keys": {
"ed25519:QJVRHWAKGH":
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
"curve25519:QJVRHWAKGH":
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
user_id: "@test2:sw1v.org",
keys: {
"ed25519:QJVRHWAKGH": "Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
"curve25519:QJVRHWAKGH": "YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "QJVRHWAKGH",
"unsigned": {
"device_display_name": "",
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "QJVRHWAKGH",
unsigned: {
device_display_name: "",
},
},
},
},
};
describe('DeviceList', function() {
let downloadSpy;
let cryptoStore;
describe("DeviceList", function () {
let downloadSpy: jest.Mock;
let cryptoStore: CryptoStore;
let deviceLists: DeviceList[] = [];
beforeEach(function() {
beforeEach(function () {
deviceLists = [];
downloadSpy = jest.fn();
cryptoStore = new MemoryCryptoStore();
});
afterEach(function() {
afterEach(function () {
for (const dl of deviceLists) {
dl.stop();
}
@@ -108,94 +99,96 @@ describe('DeviceList', function() {
function createTestDeviceList(keyDownloadChunkSize = 250) {
const baseApis = {
downloadKeysForUsers: downloadSpy,
getUserId: () => '@test1:sw1v.org',
deviceId: 'HGKAWHRVJQ',
getUserId: () => "@test1:sw1v.org",
deviceId: "HGKAWHRVJQ",
} as unknown as MatrixClient;
const mockOlm = {
verifySignature: function(key, message, signature) {},
verifySignature: function (key: string, message: string, signature: string) {},
} as unknown as OlmDevice;
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
deviceLists.push(dl);
return dl;
}
it("should successfully download and store device keys", function() {
it("should successfully download and store device keys", function () {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList("@test1:sw1v.org");
const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
return prom1.then(() => {
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
const storedKeys = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
expect(Object.keys(storedKeys)).toEqual(["HGKAWHRVJQ"]);
dl.stop();
});
});
it("should have an outdated devicelist on an invalidation while an " +
"update is in progress", function() {
it("should have an outdated devicelist on an invalidation while an " + "update is in progress", function () {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList("@test1:sw1v.org");
const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
downloadSpy.mockReset();
// outdated notif arrives while the request is in flight.
const queryDefer2 = utils.defer();
downloadSpy.mockReturnValue(queryDefer2.promise);
dl.invalidateUserDeviceList('@test1:sw1v.org');
dl.invalidateUserDeviceList("@test1:sw1v.org");
dl.refreshOutdatedDeviceLists();
dl.saveIfDirty().then(() => {
// the first request completes
queryDefer1.resolve({
failures: {},
device_keys: {
'@test1:sw1v.org': {},
},
dl.saveIfDirty()
.then(() => {
// the first request completes
queryDefer1.resolve({
failures: {},
device_keys: {
"@test1:sw1v.org": {},
},
});
return prom1;
})
.then(() => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
logger.log("Creating new devicelist to simulate app reload");
downloadSpy.mockReset();
const dl2 = createTestDeviceList();
const queryDefer3 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
dl2.stop();
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
// allow promise chain to complete
return prom3;
})
.then(() => {
const storedKeys = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
expect(Object.keys(storedKeys)).toEqual(["HGKAWHRVJQ"]);
dl.stop();
});
return prom1;
}).then(() => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
logger.log("Creating new devicelist to simulate app reload");
downloadSpy.mockReset();
const dl2 = createTestDeviceList();
const queryDefer3 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
dl2.stop();
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
// allow promise chain to complete
return prom3;
}).then(() => {
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
dl.stop();
});
});
it("should download device keys in batches", function() {
it("should download device keys in batches", function () {
const dl = createTestDeviceList(1);
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList('@test2:sw1v.org');
dl.startTrackingDeviceList("@test1:sw1v.org");
dl.startTrackingDeviceList("@test2:sw1v.org");
const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
@@ -204,16 +197,16 @@ describe('DeviceList', function() {
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toBeCalledTimes(2);
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
return prom1.then(() => {
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
const storedKeys1 = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
expect(Object.keys(storedKeys1)).toEqual(["HGKAWHRVJQ"]);
const storedKeys2 = dl.getRawStoredDevicesForUser("@test2:sw1v.org");
expect(Object.keys(storedKeys2)).toEqual(["QJVRHWAKGH"]);
dl.stop();
});
});
File diff suppressed because it is too large Load Diff
+58 -81
View File
@@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from 'jest-mock';
import { MockedObject } from "jest-mock";
import '../../../olm-loader';
import "../../../olm-loader";
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
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';
import { MatrixClient } from "../../../../src";
function makeOlmDevice() {
const cryptoStore = new MemoryCryptoStore();
@@ -31,73 +31,72 @@ function makeOlmDevice() {
return olmDevice;
}
async function setupSession(initiator, opponent) {
async function setupSession(initiator: OlmDevice, opponent: OlmDevice) {
await opponent.generateOneTimeKeys(1);
const keys = await opponent.getOneTimeKeys();
const firstKey = Object.values(keys['curve25519'])[0];
const firstKey = Object.values(keys["curve25519"])[0];
const sid = await initiator.createOutboundSession(
opponent.deviceCurve25519Key, firstKey,
);
const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key!, firstKey);
return sid;
}
describe("OlmDevice", function() {
function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
describe("OlmDevice", function () {
if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present');
logger.warn("Not running megolm unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return global.Olm.init();
});
let aliceOlmDevice: OlmDevice;
let bobOlmDevice: OlmDevice;
beforeEach(async function() {
beforeEach(async function () {
aliceOlmDevice = makeOlmDevice();
bobOlmDevice = makeOlmDevice();
await aliceOlmDevice.init();
await bobOlmDevice.init();
});
describe('olm', function() {
it("can decrypt messages", async function() {
describe("olm", function () {
it("can decrypt messages", async function () {
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
const ciphertext = await aliceOlmDevice.encryptMessage(
const ciphertext = (await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key!,
sid,
"The olm or proteus is an aquatic salamander in the family Proteidae",
) as any; // OlmDevice.encryptMessage has incorrect return type
)) as any; // OlmDevice.encryptMessage has incorrect return type
const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key!,
ciphertext.type,
ciphertext.body,
);
expect(result.payload).toEqual(
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
expect(result.payload).toEqual("The olm or proteus is an aquatic salamander in the family Proteidae");
});
it('exports picked account and olm sessions', async function() {
it("exports picked account and olm sessions", async function () {
const sessionId = await setupSession(aliceOlmDevice, bobOlmDevice);
const exported = await bobOlmDevice.export();
// At this moment only Alice (the “initiator” in setupSession) has a session
expect(exported.sessions).toEqual([]);
const MESSAGE = (
"The olm or proteus is an aquatic salamander"
+ " in the family Proteidae"
);
const ciphertext = await aliceOlmDevice.encryptMessage(
const MESSAGE = "The olm or proteus is an aquatic salamander" + " in the family Proteidae";
const ciphertext = (await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key!,
sessionId,
MESSAGE,
) as any; // OlmDevice.encryptMessage has incorrect return type
)) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedOlmDevice = makeOlmDevice();
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
@@ -113,15 +112,12 @@ describe("OlmDevice", function() {
// this time we expect Bob to have a session to export
expect(exportedAgain.sessions).toHaveLength(1);
const MESSAGE_2 = (
"In contrast to most amphibians,"
+ " the olm is entirely aquatic"
);
const ciphertext2 = await aliceOlmDevice.encryptMessage(
const MESSAGE_2 = "In contrast to most amphibians," + " the olm is entirely aquatic";
const ciphertext2 = (await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key!,
sessionId,
MESSAGE_2,
) as any; // OlmDevice.encryptMessage has incorrect return type
)) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedAgainOlmDevice = makeOlmDevice();
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
@@ -136,7 +132,7 @@ describe("OlmDevice", function() {
expect(decrypted2).toEqual(MESSAGE_2);
});
it("creates only one session at a time", async function() {
it("creates only one session at a time", async function () {
// if we call ensureOlmSessionsForDevices multiple times, it should
// only try to create one session at a time, even if the server is
// slow
@@ -152,27 +148,21 @@ describe("OlmDevice", function() {
} as unknown as MockedObject<MatrixClient>;
const devicesByUser = {
"@bob:example.com": [
DeviceInfo.fromStorage({
keys: {
"curve25519:ABCDEFG": "akey",
DeviceInfo.fromStorage(
{
keys: {
"curve25519:ABCDEFG": "akey",
},
},
}, "ABCDEFG"),
"ABCDEFG",
),
],
};
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
// start two tasks that try to ensure that there's an olm session
const promises = Promise.all([
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUser,
)),
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUser,
)),
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUser)),
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUser)),
]);
await new Promise((resolve) => {
@@ -192,7 +182,7 @@ describe("OlmDevice", function() {
expect(count).toBe(2);
});
it("avoids deadlocks when two tasks are ensuring the same devices", async function() {
it("avoids deadlocks when two tasks are ensuring the same devices", async function () {
// This test checks whether `ensureOlmSessionsForDevices` properly
// handles multiple tasks in flight ensuring some set of devices in
// common without deadlocks.
@@ -208,60 +198,47 @@ describe("OlmDevice", function() {
},
} as unknown as MockedObject<MatrixClient>;
const deviceBobA = DeviceInfo.fromStorage({
keys: {
"curve25519:BOB-A": "akey",
const deviceBobA = DeviceInfo.fromStorage(
{
keys: {
"curve25519:BOB-A": "akey",
},
},
}, "BOB-A");
const deviceBobB = DeviceInfo.fromStorage({
keys: {
"curve25519:BOB-B": "bkey",
"BOB-A",
);
const deviceBobB = DeviceInfo.fromStorage(
{
keys: {
"curve25519:BOB-B": "bkey",
},
},
}, "BOB-B");
"BOB-B",
);
// There's no required ordering of devices per user, so here we
// create two different orderings so that each task reserves a
// device the other task needs before continuing.
const devicesByUserAB = {
"@bob:example.com": [
deviceBobA,
deviceBobB,
],
"@bob:example.com": [deviceBobA, deviceBobB],
};
const devicesByUserBA = {
"@bob:example.com": [
deviceBobB,
deviceBobA,
],
"@bob:example.com": [deviceBobB, deviceBobA],
};
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserAB,
));
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserAB));
// After a single tick through the first task, it should have
// claimed ownership of all devices to avoid deadlocking others.
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserBA,
));
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserBA));
// The second task should not have changed the ownership count, as
// it's waiting on the first task.
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
// Track the tasks, but don't await them yet.
const promises = Promise.all([
task1,
task2,
]);
const promises = Promise.all([task1, task2]);
await new Promise((resolve) => {
setTimeout(resolve, 200);
+282 -259
View File
@@ -15,9 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from "jest-mock";
import '../../olm-loader';
import "../../olm-loader";
import { logger } from "../../../src/logger";
import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixClient } from "../../../src/client";
@@ -30,27 +28,31 @@ import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
import { StubStore } from "../../../src/store/stub";
import { MatrixScheduler } from '../../../src';
import { IndexedDBCryptoStore, MatrixScheduler } from "../../../src";
import { CryptoStore } from "../../../src/crypto/store/base";
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
const Olm = global.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
const ROOM_ID = '!ROOM:ID';
const ROOM_ID = "!ROOM:ID";
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
const SESSION_ID = "o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc";
const ENCRYPTED_EVENT = new MatrixEvent({
type: 'm.room.encrypted',
room_id: '!ROOM:ID',
type: "m.room.encrypted",
room_id: "!ROOM:ID",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: 'SENDER_CURVE25519',
algorithm: "m.megolm.v1.aes-sha2",
sender_key: "SENDER_CURVE25519",
session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
ciphertext:
"AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" +
"CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" +
"mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs",
},
event_id: '$event1',
event_id: "$event1",
origin_server_ts: 1507753886000,
});
@@ -59,19 +61,20 @@ const CURVE25519_KEY_BACKUP_DATA = {
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
ciphertext:
"2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" +
"6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" +
"Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" +
"SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" +
"Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" +
"ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" +
"4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" +
"C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" +
"Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" +
"QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" +
"iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg",
mac: "5lxYBHQU80M",
ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14",
},
};
@@ -80,54 +83,60 @@ const AES256_KEY_BACKUP_DATA = {
forwarded_count: 0,
is_verified: false,
session_data: {
iv: 'b3Jqqvm5S9QdmXrzssspLQ',
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce'
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd'
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0'
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r'
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P'
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K'
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd'
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA'
+ 'RgaDHkfzoA3g3aeQ',
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU',
iv: "b3Jqqvm5S9QdmXrzssspLQ",
ciphertext:
"GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce" +
"7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd" +
"EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0" +
"WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r" +
"KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P" +
"vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K" +
"YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd" +
"fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA" +
"RgaDHkfzoA3g3aeQ",
mac: "uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU",
},
};
const CURVE25519_BACKUP_INFO = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1',
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
const AES256_BACKUP_INFO = {
const AES256_BACKUP_INFO: IKeyBackupInfo = {
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: '1',
auth_data: {
// FIXME: add iv and mac
},
version: "1",
auth_data: {} as IKeyBackupInfo["auth_data"],
};
const keys = {};
const keys: Record<string, Uint8Array> = {};
function getCrossSigningKey(type) {
return keys[type];
function getCrossSigningKey(type: string) {
return Promise.resolve(keys[type]);
}
function saveCrossSigningKeys(k) {
function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
Object.assign(keys, k);
}
function makeTestClient(cryptoStore) {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
function makeTestScheduler(): MatrixScheduler {
return (["getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction"] as const).reduce(
(r, k) => {
r[k] = jest.fn();
return r;
},
{} as MatrixScheduler,
);
}
function makeTestClient(cryptoStore: CryptoStore) {
const scheduler = makeTestScheduler();
const store = new StubStore();
return new MatrixClient({
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
@@ -139,80 +148,81 @@ function makeTestClient(cryptoStore) {
cryptoStore: cryptoStore,
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
});
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
return client;
}
describe("MegolmBackup", function() {
describe("MegolmBackup", function () {
if (!global.Olm) {
logger.warn('Not running megolm backup unit tests: libolm not present');
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return Olm.init();
});
let olmDevice;
let mockOlmLib;
let mockCrypto;
let cryptoStore;
let megolmDecryption;
beforeEach(async function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto');
let olmDevice: OlmDevice;
let mockOlmLib: typeof olmlib;
let mockCrypto: Crypto;
let cryptoStore: CryptoStore;
let megolmDecryption: MegolmDecryptionClass;
beforeEach(async function () {
mockCrypto = testUtils.mock(Crypto, "Crypto");
// @ts-ignore making mock
mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager");
mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
cryptoStore = new MemoryCryptoStore();
olmDevice = new OlmDevice(cryptoStore);
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib = {} as unknown as typeof olmlib;
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
mockOlmLib.encryptMessageForDevice =
jest.fn().mockResolvedValue(undefined);
mockOlmLib.encryptMessageForDevice = jest.fn().mockResolvedValue(undefined);
});
describe("backup", function() {
let mockBaseApis;
describe("backup", function () {
let mockBaseApis: MatrixClient;
beforeEach(function() {
mockBaseApis = {};
beforeEach(function () {
mockBaseApis = {} as unknown as MatrixClient;
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
// clobber the setTimeout function to run 100x faster.
// ideally we would use lolex, but we have no oportunity
// 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);
jest.spyOn(global, "setTimeout").mockImplementation(function (f, n) {
return realSetTimeout(f!, n! / 100);
});
});
afterEach(function() {
jest.spyOn(global, 'setTimeout').mockRestore();
afterEach(function () {
jest.spyOn(global, "setTimeout").mockRestore();
});
it('automatically calls the key back up', function() {
it("automatically calls the key back up", function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// construct a fake decrypted key event via the use of a mocked
// 'crypto' implementation.
const event = new MatrixEvent({
type: 'm.room.encrypted',
type: "m.room.encrypted",
});
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
@@ -222,9 +232,9 @@ describe("MegolmBackup", function() {
};
const decryptedData = {
clearEvent: {
type: 'm.room_key',
type: "m.room_key",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
algorithm: "m.megolm.v1.aes-sha2",
room_id: ROOM_ID,
session_id: groupSession.session_id(),
session_key: groupSession.session_key(),
@@ -234,23 +244,27 @@ describe("MegolmBackup", function() {
claimedEd25519Key: "SENDER_ED25519",
};
mockCrypto.decryptEvent = function() {
mockCrypto.decryptEvent = function () {
return Promise.resolve(decryptedData);
};
mockCrypto.cancelRoomKeyRequest = function() {};
mockCrypto.cancelRoomKeyRequest = function () {};
// @ts-ignore readonly field write
mockCrypto.backupManager = {
backupGroupSession: jest.fn(),
};
return event.attemptDecryption(mockCrypto).then(() => {
return megolmDecryption.onRoomKeyEvent(event);
}).then(() => {
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled();
});
return event
.attemptDecryption(mockCrypto)
.then(() => {
return megolmDecryption.onRoomKeyEvent(event);
})
.then(() => {
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled();
});
});
it('sends backups to the server (Curve25519 version)', function() {
it("sends backups to the server (Curve25519 version)", function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
@@ -259,64 +273,62 @@ describe("MegolmBackup", function() {
const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
return client
.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
txn);
});
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn,
);
});
})
.then(async () => {
await client.enableKeyBackup({
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1',
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
++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 T);
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(queryParams?.version).toBe("1");
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 T);
return Promise.resolve({});
};
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -329,7 +341,7 @@ describe("MegolmBackup", function() {
});
});
it('sends backups to the server (AES-256 version)', function() {
it("sends backups to the server (AES-256 version)", function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
@@ -338,42 +350,42 @@ describe("MegolmBackup", function() {
const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
return client
.initCrypto()
.then(() => {
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
})
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
txn);
});
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn,
);
});
})
.then(async () => {
await client.enableKeyBackup({
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: '1',
version: "1",
auth_data: {
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
@@ -381,25 +393,23 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
++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 T);
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(queryParams?.version).toBe("1");
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 T);
return Promise.resolve({});
};
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -412,7 +422,7 @@ describe("MegolmBackup", function() {
});
});
it('signs backups with the cross-signing master key', async function() {
it("signs backups with the cross-signing master key", async function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
@@ -421,26 +431,29 @@ describe("MegolmBackup", function() {
const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
client.uploadDeviceSigningKeys = async function(e) {return {};};
client.uploadKeySignatures = async function(e) {return { failures: {} };};
client.uploadDeviceSigningKeys = async function (e) {
return {};
};
client.uploadKeySignatures = async function (e) {
return { failures: {} };
};
await resetCrossSigningKeys(client);
let numCalls = 0;
await Promise.all([
new Promise<void>((resolve, reject) => {
let backupInfo;
client.http.authedRequest = function(
method, path, queryParams, data, opts,
) {
let backupInfo: Record<string, any> | BodyInit | undefined;
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls === 1) {
@@ -449,7 +462,9 @@ describe("MegolmBackup", function() {
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
(data as Record<string, any>).auth_data, client.getCrossSigningId()!, "@alice:bar",
(data as Record<string, any>).auth_data,
client.getCrossSigningId()!,
"@alice:bar",
);
} catch (e) {
reject(e);
@@ -480,16 +495,13 @@ describe("MegolmBackup", function() {
client.stopClient();
});
it('retries when a backup fails', async function() {
it("retries when a backup fails", async function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const scheduler = makeTestScheduler();
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
@@ -502,39 +514,40 @@ describe("MegolmBackup", function() {
deviceId: "device",
cryptoStore: cryptoStore,
});
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
await cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
txn);
});
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn,
);
});
await client.enableKeyBackup({
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1',
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
@@ -542,30 +555,26 @@ describe("MegolmBackup", function() {
let numCalls = 0;
await new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
++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 T);
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(queryParams?.version).toBe("1");
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 T);
return Promise.resolve({});
} else {
return Promise.reject(
new Error("this is an expected failure"),
);
return Promise.reject(new Error("this is an expected failure"));
}
};
return client.crypto!.backupManager.backupGroupSession(
@@ -578,66 +587,73 @@ describe("MegolmBackup", function() {
});
});
describe("restore", function() {
let client;
describe("restore", function () {
let client: MatrixClient;
beforeEach(function() {
beforeEach(function () {
client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
});
afterEach(function() {
afterEach(function () {
client.stopClient();
});
it('can restore from backup (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
it("can restore from backup (Curve25519 version)", function () {
client.http.authedRequest = function () {
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
});
return client
.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
CURVE25519_BACKUP_INFO,
)
.then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
})
.then((res) => {
expect(res.clearEvent.content).toEqual("testytest");
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
});
});
it('can restore from backup (AES-256 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(AES256_KEY_BACKUP_DATA);
it("can restore from backup (AES-256 version)", function () {
client.http.authedRequest = function () {
return Promise.resolve<any>(AES256_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
AES256_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
});
return client
.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
AES256_BACKUP_INFO,
)
.then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
})
.then((res) => {
expect(res.clearEvent.content).toEqual("testytest");
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
});
});
it('can restore backup by room (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve({
it("can restore backup by room (Curve25519 version)", function () {
client.http.authedRequest = function () {
return Promise.resolve<any>({
rooms: {
[ROOM_ID]: {
sessions: {
@@ -647,30 +663,35 @@ describe("MegolmBackup", function() {
},
});
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
return client
.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null!,
null!,
CURVE25519_BACKUP_INFO,
)
.then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
})
.then((res) => {
expect(res.clearEvent.content).toEqual("testytest");
});
});
it('has working cache functions', async function() {
it("has working cache functions", async function () {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client.crypto.storeSessionBackupPrivateKey(key);
const result = await client.crypto.getSessionBackupPrivateKey();
expect(new Uint8Array(result)).toEqual(key);
await client.crypto!.storeSessionBackupPrivateKey(key);
const result = await client.crypto!.getSessionBackupPrivateKey();
expect(new Uint8Array(result!)).toEqual(key);
});
it('caches session backup keys as it encounters them', async function() {
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
it("caches session backup keys as it encounters them", async function () {
const cachedNull = await client.crypto!.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull();
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
client.http.authedRequest = function () {
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
await new Promise<void>((resolve) => {
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
@@ -679,33 +700,32 @@ describe("MegolmBackup", function() {
{ cacheCompleteCallback: resolve },
);
});
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
expect(cachedKey).not.toBeNull();
});
it("fails if an known algorithm is used", async function() {
it("fails if an known algorithm is used", async function () {
const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
algorithm: "this.algorithm.does.not.exist",
});
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
client.http.authedRequest = function () {
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
};
await expect(client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BAD_BACKUP_INFO,
)).rejects.toThrow();
await expect(
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BAD_BACKUP_INFO,
),
).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 scheduler = makeTestScheduler();
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
@@ -718,6 +738,9 @@ describe("MegolmBackup", function() {
deviceId: "device",
cryptoStore,
});
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
await client.initCrypto();
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
+243 -221
View File
@@ -15,26 +15,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../olm-loader';
import anotherjson from 'another-json';
import { PkSigning } from '@matrix-org/olm';
import "../../olm-loader";
import anotherjson from "another-json";
import { PkSigning } from "@matrix-org/olm";
import HttpBackend from "matrix-mock-request";
import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixError } from '../../../src/http-api';
import { logger } from '../../../src/logger';
import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client';
import { CryptoEvent } from '../../../src/crypto';
import { IDevice } from '../../../src/crypto/deviceinfo';
import { TestClient } from '../../TestClient';
import { MatrixError } from "../../../src/http-api";
import { logger } from "../../../src/logger";
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
import { CryptoEvent, IBootstrapCrossSigningOpts } from "../../../src/crypto";
import { IDevice } from "../../../src/crypto/deviceinfo";
import { TestClient } from "../../TestClient";
import { resetCrossSigningKeys } from "./crypto-utils";
const PUSH_RULES_RESPONSE = {
const PUSH_RULES_RESPONSE: Response = {
method: "GET",
path: "/pushrules/",
data: {},
};
const filterResponse = function(userId) {
const filterResponse = function (userId: string): Response {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
@@ -43,33 +44,37 @@ const filterResponse = function(userId) {
};
};
function setHttpResponses(httpBackend, responses) {
responses.forEach(response => {
httpBackend
.when(response.method, response.path)
.respond(200, response.data);
interface Response {
method: "GET" | "PUT" | "POST" | "DELETE";
path: string;
data: object;
}
function setHttpResponses(httpBackend: HttpBackend, responses: Response[]) {
responses.forEach((response) => {
httpBackend.when(response.method, response.path).respond(200, response.data);
});
}
async function makeTestClient(
userInfo: { userId: string, deviceId: string},
userInfo: { userId: string; deviceId: string },
options: Partial<ICreateClientOpts> = {},
keys = {},
keys: Record<string, Uint8Array> = {},
) {
function getCrossSigningKey(type) {
return keys[type];
function getCrossSigningKey(type: string) {
return keys[type] ?? null;
}
function saveCrossSigningKeys(k) {
function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
Object.assign(keys, k);
}
options.cryptoCallbacks = Object.assign(
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
);
const testClient = new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
{},
{ getCrossSigningKey, saveCrossSigningKeys },
options.cryptoCallbacks || {},
);
const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
const client = testClient.client;
await client.initCrypto();
@@ -77,24 +82,25 @@ async function makeTestClient(
return { client, httpBackend: testClient.httpBackend };
}
describe("Cross Signing", function() {
describe("Cross Signing", function () {
if (!global.Olm) {
logger.warn('Not running megolm backup unit tests: libolm not present');
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return global.Olm.init();
});
it("should sign the master key with the device key", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should sign the master key with the device key", async function () {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
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: {} });
@@ -102,24 +108,22 @@ describe("Cross Signing", function() {
alice.getAccountDataFromServer = async <T>() => ({} as T);
// set Alice's cross-signing key
await alice.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { await func({}); },
authUploadDeviceSigningKeys: async (func) => {
await func({});
},
});
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
alice.stopClient();
});
it("should abort bootstrap if device signing auth fails", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should abort bootstrap if device signing auth fails", async function () {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
alice.uploadDeviceSigningKeys = async (auth, keys) => {
const errorResponse = {
session: "sessionId",
flows: [
{
stages: [
"m.login.password",
],
stages: ["m.login.password"],
},
],
params: {},
@@ -141,8 +145,10 @@ describe("Cross Signing", function() {
};
alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({});
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
const authUploadDeviceSigningKeys = async func => await func({});
alice.getAccountDataFromServer = async <T extends { [k: string]: any }>(): Promise<T | null> => ({} as T);
const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
await func({});
};
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
// through failure, stopping before actually applying changes.
@@ -160,10 +166,8 @@ describe("Cross Signing", function() {
alice.stopClient();
});
it("should upload a signature when a user is verified", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should upload a signature when a user is verified", async function () {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key
@@ -195,18 +199,14 @@ describe("Cross Signing", function() {
alice.stopClient();
});
it.skip("should get cross-signing keys from sync", async function() {
it.skip("should get cross-signing keys from sync", async function () {
const masterKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1,
0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0x17, 0xb5,
0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]);
const { client: alice, httpBackend } = await makeTestClient(
@@ -214,8 +214,8 @@ describe("Cross Signing", function() {
{
cryptoCallbacks: {
// will be called to sign our own device
getCrossSigningKey: async type => {
if (type === 'master') {
getCrossSigningKey: async (type) => {
if (type === "master") {
return masterKey;
} else {
return selfSigningKey;
@@ -239,11 +239,10 @@ describe("Cross Signing", function() {
try {
await olmlib.verifySignature(
alice.crypto!.olmDevice,
content["@alice:example.com"][
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
],
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,8 +257,7 @@ describe("Cross Signing", function() {
});
// @ts-ignore private property
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
.Osborne2;
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
device_id: "Osborne2",
@@ -267,15 +265,10 @@ describe("Cross Signing", function() {
algorithms: deviceInfo.algorithms,
};
await alice.crypto!.signObject(aliceDevice);
olmlib.pkSign(
aliceDevice as ISignedKey,
selfSigningKey as unknown as PkSigning,
"@alice:example.com",
'',
);
olmlib.pkSign(aliceDevice as ISignedKey, selfSigningKey as unknown as PkSigning, "@alice:example.com", "");
// feed sync result that includes master key, ssk, device key
const responses = [
const responses: Response[] = [
PUSH_RULES_RESPONSE,
{
method: "POST",
@@ -294,10 +287,7 @@ describe("Cross Signing", function() {
data: {
next_batch: "abcdefg",
device_lists: {
changed: [
"@alice:example.com",
"@bob:example.com",
],
changed: ["@alice:example.com", "@bob:example.com"],
},
},
},
@@ -305,35 +295,35 @@ describe("Cross Signing", function() {
method: "POST",
path: "/keys/query",
data: {
"failures": {},
"device_keys": {
failures: {},
device_keys: {
"@alice:example.com": {
"Osborne2": aliceDevice,
Osborne2: aliceDevice,
},
},
"master_keys": {
master_keys: {
"@alice:example.com": {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
},
},
},
"self_signing_keys": {
self_signing_keys: {
"@alice:example.com": {
user_id: "@alice:example.com",
usage: ["self-signing"],
keys: {
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
},
signatures: {
"@alice:example.com": {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs"
+ "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA",
"Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs" +
"Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA",
},
},
},
@@ -373,10 +363,8 @@ describe("Cross Signing", function() {
alice.stopClient();
});
it("should use trust chain to determine device verification", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should use trust chain to determine device verification", async function () {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key
@@ -463,8 +451,8 @@ describe("Cross Signing", function() {
alice.stopClient();
});
it.skip("should trust signatures received from other devices", async function() {
const aliceKeys: Record<string, PkSigning> = {};
it.skip("should trust signatures received from other devices", async function () {
const aliceKeys: Record<string, Uint8Array> = {};
const { client: alice, httpBackend } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
undefined,
@@ -479,10 +467,8 @@ describe("Cross Signing", function() {
await resetCrossSigningKeys(alice);
const selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66, 0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0, 0x17, 0xb5,
0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49, 0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]);
const keyChangePromise = new Promise<void>((resolve, reject) => {
@@ -494,8 +480,7 @@ describe("Cross Signing", function() {
});
// @ts-ignore private property
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"]
.Osborne2;
const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
device_id: "Osborne2",
@@ -527,29 +512,23 @@ describe("Cross Signing", function() {
verified: 0,
known: false,
};
olmlib.pkSign(
bobDevice,
selfSigningKey as unknown as PkSigning,
"@bob:example.com",
'',
);
olmlib.pkSign(bobDevice, selfSigningKey as unknown as PkSigning, "@bob:example.com", "");
const bobMaster: ICrossSigningKey = {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
},
};
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com", '');
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com", "");
// Alice downloads Bob's keys
// - device key
// - ssk
// - master key signed by her usk (pretend that it was signed by another
// of Alice's devices)
const responses = [
const responses: Response[] = [
PUSH_RULES_RESPONSE,
{
method: "POST",
@@ -568,9 +547,7 @@ describe("Cross Signing", function() {
data: {
next_batch: "abcdefg",
device_lists: {
changed: [
"@bob:example.com",
],
changed: ["@bob:example.com"],
},
},
},
@@ -578,31 +555,31 @@ describe("Cross Signing", function() {
method: "POST",
path: "/keys/query",
data: {
"failures": {},
"device_keys": {
failures: {},
device_keys: {
"@alice:example.com": {
"Osborne2": aliceDevice,
Osborne2: aliceDevice,
},
"@bob:example.com": {
"Dynabook": bobDevice,
Dynabook: bobDevice,
},
},
"master_keys": {
master_keys: {
"@bob:example.com": bobMaster,
},
"self_signing_keys": {
self_signing_keys: {
"@bob:example.com": {
user_id: "@bob:example.com",
usage: ["self-signing"],
keys: {
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
},
signatures: {
"@bob:example.com": {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB"
+ "LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ",
"2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB" +
"LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ",
},
},
},
@@ -638,10 +615,8 @@ describe("Cross Signing", function() {
alice.stopClient();
});
it("should dis-trust an unsigned device", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should dis-trust an unsigned device", async function () {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
// set Alice's cross-signing key
@@ -708,10 +683,8 @@ describe("Cross Signing", function() {
alice.stopClient();
});
it("should dis-trust a user when their ssk changes", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should dis-trust a user when their ssk changes", async function () {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
await resetCrossSigningKeys(alice);
@@ -852,8 +825,8 @@ describe("Cross Signing", function() {
alice.stopClient();
});
it("should offer to upgrade device verifications to cross-signing", async function() {
let upgradeResolveFunc;
it("should offer to upgrade device verifications to cross-signing", async function () {
let upgradeResolveFunc: Function;
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
@@ -866,11 +839,8 @@ describe("Cross Signing", function() {
},
},
},
);
const { client: bob } = await makeTestClient(
{ userId: "@bob:example.com", deviceId: "Dynabook" },
);
const { client: bob } = await makeTestClient({ userId: "@bob:example.com", deviceId: "Dynabook" });
bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = async () => ({ failures: {} });
@@ -887,10 +857,7 @@ describe("Cross Signing", function() {
known: true,
},
});
alice.crypto!.deviceList.storeCrossSigningForUser(
"@bob:example.com",
bob.crypto!.crossSigningInfo.toStorage(),
);
alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", bob.crypto!.crossSigningInfo.toStorage());
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
@@ -909,8 +876,9 @@ 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();
@@ -932,91 +900,83 @@ describe("Cross Signing", function() {
bob.stopClient();
});
it(
"should observe that our own device is cross-signed, even if this device doesn't trust the key",
async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
it("should observe that our own device is cross-signed, even if this device doesn't trust the key", async function () {
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,
// 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, but doesn't trust them yet
alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey,
},
},
};
const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK));
aliceSSK.signatures = {
self_signing: aliceSSK,
},
firstUse: true,
crossSigningVerifiedBefore: false,
});
// Alice has a second device that's cross-signed
const aliceDeviceId = "Dynabook";
const aliceUnsignedDevice = {
user_id: "@alice:example.com",
device_id: aliceDeviceId,
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": "somePubkey",
"ed25519:Dynabook": "someOtherPubkey",
},
};
const sig = aliceSigning.sign(anotherjson.stringify(aliceUnsignedDevice));
const aliceCrossSignedDevice: IDevice = {
...aliceUnsignedDevice,
verified: 0,
known: false,
signatures: {
"@alice:example.com": {
["ed25519:" + aliceMasterPubkey]: sskSig,
["ed25519:" + alicePubkey]: sig,
},
};
},
};
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
[aliceDeviceId]: aliceCrossSignedDevice,
});
// Alice's device downloads the keys, but doesn't trust them yet
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,
});
// We don't trust the cross-signing keys yet...
expect(alice.checkDeviceTrust("@alice:example.com", aliceDeviceId).isCrossSigningVerified()).toBeFalsy();
// ... but we do acknowledge that the device is signed by them
expect(alice.checkIfOwnDeviceCrossSigned(aliceDeviceId)).toBeTruthy();
alice.stopClient();
});
// Alice has a second device that's cross-signed
const aliceDeviceId = 'Dynabook';
const aliceUnsignedDevice = {
user_id: "@alice:example.com",
device_id: aliceDeviceId,
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": "somePubkey",
"ed25519:Dynabook": "someOtherPubkey",
},
};
const sig = aliceSigning.sign(anotherjson.stringify(aliceUnsignedDevice));
const aliceCrossSignedDevice: IDevice = {
...aliceUnsignedDevice,
verified: 0,
known: false,
signatures: {
"@alice:example.com": {
["ed25519:" + alicePubkey]: sig,
},
} };
alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
[aliceDeviceId]: aliceCrossSignedDevice,
});
// We don't trust the cross-signing keys yet...
expect(
alice.checkDeviceTrust("@alice:example.com", aliceDeviceId).isCrossSigningVerified(),
).toBeFalsy();
// ... but we do acknowledge that the device is signed by them
expect(alice.checkIfOwnDeviceCrossSigned(aliceDeviceId)).toBeTruthy();
alice.stopClient();
},
);
it("should observe that our own device isn't cross-signed", async function() {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should observe that our own device isn't cross-signed", async function () {
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
@@ -1076,9 +1036,7 @@ describe("Cross Signing", function() {
});
it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => {
const { client: alice } = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
alice.uploadDeviceSigningKeys = async () => ({});
alice.uploadKeySignatures = async () => ({ failures: {} });
@@ -1129,3 +1087,67 @@ describe("Cross Signing", function() {
alice.stopClient();
});
});
describe("userHasCrossSigningKeys", function () {
if (!global.Olm) {
return;
}
beforeAll(() => {
return global.Olm.init();
});
let aliceClient: MatrixClient;
let httpBackend: HttpBackend;
beforeEach(async () => {
const testClient = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
aliceClient = testClient.client;
httpBackend = testClient.httpBackend;
});
afterEach(() => {
aliceClient.stopClient();
});
it("should download devices and return true if one is a cross-signing key", async () => {
httpBackend.when("POST", "/keys/query").respond(200, {
master_keys: {
"@alice:example.com": {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
},
},
},
});
let result: boolean;
await Promise.all([
httpBackend.flush("/keys/query"),
aliceClient.userHasCrossSigningKeys().then((res) => {
result = res;
}),
]);
expect(result!).toBeTruthy();
});
it("should download devices and return false if there is no cross-signing key", async () => {
httpBackend.when("POST", "/keys/query").respond(200, {});
let result: boolean;
await Promise.all([
httpBackend.flush("/keys/query"),
aliceClient.userHasCrossSigningKeys().then((res) => {
result = res;
}),
]);
expect(result!).toBeFalsy();
});
it("throws an error if crypto is disabled", () => {
aliceClient["cryptoBackend"] = undefined;
expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled");
});
});
+13 -14
View File
@@ -1,34 +1,33 @@
import { IRecoveryKey } from '../../../src/crypto/api';
import { CrossSigningLevel } from '../../../src/crypto/CrossSigning';
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
import { IRecoveryKey } from "../../../src/crypto/api";
import { CrossSigningLevel } from "../../../src/crypto/CrossSigning";
import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
import { MatrixClient } from "../../../src";
import { CryptoEvent } from "../../../src/crypto";
// needs to be phased out and replaced with bootstrapSecretStorage,
// but that is doing too much extra stuff for it to be an easy transition.
export async function resetCrossSigningKeys(
client,
{ level }: { level?: CrossSigningLevel} = {},
client: MatrixClient,
{ level }: { level?: CrossSigningLevel } = {},
): Promise<void> {
const crypto = client.crypto;
const crypto = client.crypto!;
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
try {
await crypto.crossSigningInfo.resetKeys(level);
await crypto.signObject(crypto.crossSigningInfo.keys.master);
// write a copy locally so we know these are trusted keys
await crypto.cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
crypto.cryptoStore.storeCrossSigningKeys(
txn, crypto.crossSigningInfo.keys);
},
);
await crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
crypto.cryptoStore.storeCrossSigningKeys(txn, crypto.crossSigningInfo.keys);
});
} catch (e) {
// If anything failed here, revert the keys so we know to try again from the start
// next time.
crypto.crossSigningInfo.keys = oldKeys;
throw e;
}
crypto.emit("crossSigning.keysChanged", {});
crypto.emit(CryptoEvent.KeysChanged, {});
// @ts-ignore
await crypto.afterCrossSigningLocalKeyChange();
}
+25 -30
View File
@@ -14,33 +14,30 @@ 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';
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');
logger.warn("Not running dehydration unit tests: libolm not present");
return;
}
beforeAll(function() {
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 alice = new TestClient("@alice:example.com", "Osborne2", undefined, undefined, {
cryptoCallbacks: {
getDehydrationKey: async (t) => key,
},
);
});
const dehydratedDevice = new Olm.Account();
dehydratedDevice.create();
@@ -56,25 +53,20 @@ describe("Dehydration", () => {
success: true,
});
expect((await Promise.all([
alice.client.rehydrateDevice(),
alice.httpBackend.flushAllExpected(),
]))[0])
.toEqual("ABCDEFG");
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,
},
const alice = new TestClient("@alice:example.com", "Osborne2", undefined, undefined, {
cryptoCallbacks: {
getDehydrationKey: async (t) => key,
},
);
});
await alice.client.initCrypto();
@@ -84,7 +76,8 @@ describe("Dehydration", () => {
let pickledAccount = "";
alice.httpBackend.when("PUT", "/dehydrated_device")
alice.httpBackend
.when("PUT", "/dehydrated_device")
.check((req) => {
expect(req.data.device_data).toMatchObject({
algorithm: DEHYDRATION_ALGORITHM,
@@ -95,7 +88,8 @@ describe("Dehydration", () => {
.respond(200, {
device_id: "ABCDEFG",
});
alice.httpBackend.when("POST", "/keys/upload/ABCDEFG")
alice.httpBackend
.when("POST", "/keys/upload/ABCDEFG")
.check((req) => {
expect(req.data).toMatchObject({
"device_keys": expect.objectContaining({
@@ -119,11 +113,12 @@ describe("Dehydration", () => {
.respond(200, {});
try {
const deviceId =
(await Promise.all([
const deviceId = (
await Promise.all([
alice.client.createDehydratedDevice(new Uint8Array(key), {}),
alice.httpBackend.flushAllExpected(),
]))[0];
])
)[0];
expect(deviceId).toEqual("ABCDEFG");
expect(deviceId).not.toEqual("");
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
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';
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";
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
import "fake-indexeddb/auto";
import "jest-localstorage-mock";
const requests = [
{
@@ -46,54 +46,46 @@ const requests = [
requestId: "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" },
],
recipients: [{ userId: "@becca:example.com", deviceId: "foobarbaz" }],
},
];
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("Outgoing room key requests [%s]", function(name, dbFactory) {
])("Outgoing room key requests [%s]", function (name, dbFactory) {
let store: CryptoStore;
beforeAll(async () => {
store = dbFactory();
await store.startup();
await Promise.all(requests.map((request) =>
store.getOrAddOutgoingRoomKeyRequest(request),
));
await Promise.all(requests.map((request) => store.getOrAddOutgoingRoomKeyRequest(request)));
});
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
async () => {
const r = await
store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
expect(r).toHaveLength(2);
requests.filter((e) => e.state === RoomKeyRequestState.Sent).forEach((e) => {
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state", async () => {
const r = await store.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent);
expect(r).toHaveLength(2);
requests
.filter((e) => e.state === RoomKeyRequestState.Sent)
.forEach((e) => {
expect(r).toContainEqual(e);
});
});
});
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]);
});
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(requests).toContainEqual(r);
});
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(requests).toContainEqual(r);
});
});
+177 -163
View File
@@ -14,56 +14,70 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../olm-loader';
import "../../olm-loader";
import * as olmlib from "../../../src/crypto/olmlib";
import { IObject } from "../../../src/crypto/olmlib";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
import { MatrixEvent } from "../../../src/models/event";
import { TestClient } from '../../TestClient';
import { makeTestClients } from './verification/util';
import { TestClient } from "../../TestClient";
import { makeTestClients } from "./verification/util";
import { encryptAES } from "../../../src/crypto/aes";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from '../../../src/logger';
import { ClientEvent, ICreateClientOpts } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
import { ISecretStorageKeyInfo } from "../../../src/crypto/api";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { ISignatures } from "../../../src/@types/signed";
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
)).client;
async function makeTestClient(
userInfo: { userId: string; deviceId: string },
options: Partial<ICreateClientOpts> = {},
) {
const client = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options).client;
// Make it seem as if we've synced and thus the store can be trusted to
// contain valid account data.
client.isInitialSyncComplete = function() {
client.isInitialSyncComplete = function () {
return true;
};
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;
}
// Wrapper around pkSign to return a signed object. pkSign returns the
// signature, rather than the signed object.
function sign(obj, key, userId) {
olmlib.pkSign(obj, key, userId, '');
return obj;
function sign<T extends IObject | ICurve25519AuthData>(
obj: T,
key: Uint8Array,
userId: string,
): T & {
signatures: ISignatures;
unsigned?: object;
} {
olmlib.pkSign(obj, key, userId, "");
return obj as T & {
signatures: ISignatures;
unsigned?: object;
};
}
describe("Secrets", function() {
describe("Secrets", function () {
if (!global.Olm) {
logger.warn('Not running megolm backup unit tests: libolm not present');
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return global.Olm.init();
});
it("should store and retrieve a secret", async function() {
it("should store and retrieve a secret", async function () {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
@@ -73,22 +87,22 @@ describe("Secrets", function() {
const signingkeyInfo = {
user_id: "@alice:example.com",
usage: ['master'],
usage: ["master"],
keys: {
['ed25519:' + signingPubKey]: signingPubKey,
["ed25519:" + signingPubKey]: signingPubKey,
},
};
const getKey = jest.fn().mockImplementation(async e => {
const getKey = jest.fn().mockImplementation(async (e) => {
expect(Object.keys(e.keys)).toEqual(["abc"]);
return ['abc', key];
return ["abc", key];
});
const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
getCrossSigningKey: async t => signingKey,
getCrossSigningKey: async (t) => signingKey,
getSecretStorageKey: getKey,
},
},
@@ -99,21 +113,20 @@ describe("Secrets", function() {
const secretStorage = alice.crypto!.secretStorage;
jest.spyOn(alice, 'setAccountData').mockImplementation(
async function(eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
return {};
});
jest.spyOn(alice, "setAccountData").mockImplementation(async function (eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
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({
@@ -133,54 +146,48 @@ describe("Secrets", function() {
alice.stopClient();
});
it("should throw if given a key that doesn't exist", async function() {
const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should throw if given a key that doesn't exist", async function () {
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
try {
await alice.storeSecret("foo", "bar", ["this secret does not exist"]);
// should be able to use expect(...).toThrow() but mocha still fails
// the test even when it throws for reasons I have no inclination to debug
expect(true).toBeFalsy();
} catch (e) {
}
} catch (e) {}
alice.stopClient();
});
it("should refuse to encrypt with zero keys", async function() {
const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should refuse to encrypt with zero keys", async function () {
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
try {
await alice.storeSecret("foo", "bar", []);
expect(true).toBeFalsy();
} catch (e) {
}
} catch (e) {}
alice.stopClient();
});
it("should encrypt with default key if keys is null", async function() {
it("should encrypt with default key if keys is null", async function () {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn().mockImplementation(async e => {
const getKey = jest.fn().mockImplementation(async (e) => {
expect(Object.keys(e.keys)).toEqual([newKeyId]);
return [newKeyId, key];
});
let keys = {};
let keys: Record<string, Uint8Array> = {};
const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
getCrossSigningKey: t => keys[t],
saveCrossSigningKeys: k => keys = k,
getCrossSigningKey: (t) => Promise.resolve(keys[t]),
saveCrossSigningKeys: (k) => (keys = k),
getSecretStorageKey: getKey,
},
},
);
alice.setAccountData = async function(eventType, contents) {
alice.setAccountData = async function (eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
@@ -191,33 +198,31 @@ describe("Secrets", function() {
};
resetCrossSigningKeys(alice);
const { keyId: newKeyId } = await alice.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES, { pubkey: undefined, key: undefined },
);
const { keyId: newKeyId } = await alice.addSecretStorageKey(SECRET_STORAGE_ALGORITHM_V1_AES, {
pubkey: undefined,
key: undefined,
});
// we don't await on this because it waits for the event to come down the sync
// which won't happen in the test setup
alice.setDefaultSecretStorageKeyId(newKeyId);
await alice.storeSecret("foo", "bar");
const accountData = alice.getAccountData('foo');
const accountData = alice.getAccountData("foo");
expect(accountData!.getContent().encrypted).toBeTruthy();
alice.stopClient();
});
it("should refuse to encrypt if no keys given and no default key", async function() {
const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
);
it("should refuse to encrypt if no keys given and no default key", async function () {
const alice = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" });
try {
await alice.storeSecret("foo", "bar");
expect(true).toBeFalsy();
} catch (e) {
}
} catch (e) {}
alice.stopClient();
});
it("should request secrets from other clients", async function() {
it("should request secrets from other clients", async function () {
const [[osborne2, vax], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
@@ -227,7 +232,7 @@ describe("Secrets", function() {
cryptoCallbacks: {
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
expect(secretName).toBe("foo");
return "bar";
return Promise.resolve("bar");
},
},
},
@@ -238,7 +243,7 @@ describe("Secrets", function() {
const secretStorage = osborne2.client.crypto!.secretStorage;
osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": {
VAX: {
known: false,
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
@@ -249,7 +254,7 @@ describe("Secrets", function() {
},
});
vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": {
Osborne2: {
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
verified: 0,
known: false,
@@ -280,30 +285,20 @@ describe("Secrets", function() {
clearTestClientTimeouts();
});
describe("bootstrap", function() {
describe("bootstrap", function () {
// keys used in some of the tests
const XSK = new Uint8Array(
olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="),
);
const XSK = new Uint8Array(olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="));
const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0";
const USK = new Uint8Array(
olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="),
);
const USK = new Uint8Array(olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="));
const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU";
const SSK = new Uint8Array(
olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="),
);
const SSK = new Uint8Array(olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="));
const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q";
const SSSSKey = new Uint8Array(
olmlib.decodeBase64(
"XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0=",
),
);
const SSSSKey = new Uint8Array(olmlib.decodeBase64("XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0="));
it("bootstraps when no storage or cross-signing keys locally", async function() {
it("bootstraps when no storage or cross-signing keys locally", async function () {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn().mockImplementation(async e => {
const getKey = jest.fn().mockImplementation(async (e) => {
return [Object.keys(e.keys)[0], key];
});
@@ -320,20 +315,20 @@ describe("Secrets", function() {
);
bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
bob.setAccountData = async function(eventType, contents) {
bob.setAccountData = async function (eventType, contents) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.store.storeAccountDataEvents([event]);
this.emit(ClientEvent.AccountData, event);
return {};
};
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { await func({}); },
authUploadDeviceSigningKeys: async (func) => {
await func({});
},
});
await bob.bootstrapSecretStorage({
createSecretStorageKey,
@@ -343,53 +338,53 @@ describe("Secrets", function() {
const secretStorage = bob.crypto!.secretStorage;
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
bob.stopClient();
});
it("bootstraps when cross-signing keys in secret storage", async function() {
it("bootstraps when cross-signing keys in secret storage", async function () {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
const bob = await makeTestClient(
const bob: MatrixClient = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: async request => {
getSecretStorageKey: async (request) => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
return [defaultKeyId!, storagePrivateKey];
},
},
},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = async () => ({ failures: {} });
bob.setAccountData = async function (eventType, contents) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
this.store.storeAccountDataEvents([event]);
this.emit(ClientEvent.AccountData, event);
return {};
};
bob.crypto.backupManager.checkKeyBackup = async () => {};
bob.crypto!.backupManager.checkKeyBackup = async () => null;
const crossSigning = bob.crypto.crossSigningInfo;
const secretStorage = bob.crypto.secretStorage;
const crossSigning = bob.crypto!.crossSigningInfo;
const secretStorage = bob.crypto!.secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
authUploadDeviceSigningKeys: async (func) => {
await func({});
},
});
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({
@@ -400,37 +395,35 @@ describe("Secrets", function() {
});
// Clear local cross-signing keys and read from secret storage
bob.crypto.deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
bob.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", crossSigning.toStorage());
crossSigning.keys = {};
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
authUploadDeviceSigningKeys: async (func) => {
await func({});
},
});
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
bob.stopClient();
});
it("adds passphrase checking if it's lacking", async function() {
it("adds passphrase checking if it's lacking", async function () {
let crossSigningKeys: Record<string, Uint8Array> = {
master: XSK,
user_signing: USK,
self_signing: SSK,
};
const secretStorageKeys = {
const secretStorageKeys: Record<string, Uint8Array> = {
key_id: SSSSKey,
};
const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
getCrossSigningKey: async t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getCrossSigningKey: async (t) => crossSigningKeys[t],
saveCrossSigningKeys: (k) => (crossSigningKeys = k),
getSecretStorageKey: async ({ keys }, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
@@ -498,32 +491,44 @@ describe("Secrets", function() {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
self_signing: sign<ICrossSigningKey>(
{
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
XSK,
"@alice:example.com",
),
user_signing: sign<ICrossSigningKey>(
{
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
},
}, XSK, "@alice:example.com"),
XSK,
"@alice:example.com",
),
},
});
alice.getKeyBackupVersion = async () => {
return {
version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
}, XSK, "@alice:example.com"),
auth_data: sign(
{
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
},
XSK,
"@alice:example.com",
),
};
};
alice.setAccountData = async function(name, data) {
alice.setAccountData = async function (name, data) {
const event = new MatrixEvent({
type: name,
content: data,
@@ -535,11 +540,9 @@ describe("Secrets", function() {
await alice.bootstrapSecretStorage({});
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent())
.toEqual({ key: "key_id" });
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()).toEqual({ key: "key_id" });
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
expect(keyInfo.algorithm)
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({
algorithm: "m.pbkdf2",
iterations: 500000,
@@ -547,25 +550,24 @@ describe("Secrets", function() {
});
expect(keyInfo).toHaveProperty("iv");
expect(keyInfo).toHaveProperty("mac");
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
.toBeTruthy();
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo)).toBeTruthy();
alice.stopClient();
});
it("fixes backup keys in the wrong format", async function() {
it("fixes backup keys in the wrong format", async function () {
let crossSigningKeys: Record<string, Uint8Array> = {
master: XSK,
user_signing: USK,
self_signing: SSK,
};
const secretStorageKeys = {
const secretStorageKeys: Record<string, Uint8Array> = {
key_id: SSSSKey,
};
const alice = await makeTestClient(
{ userId: "@alice:example.com", deviceId: "Osborne2" },
{
cryptoCallbacks: {
getCrossSigningKey: async t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getCrossSigningKey: async (t) => crossSigningKeys[t],
saveCrossSigningKeys: (k) => (crossSigningKeys = k),
getSecretStorageKey: async ({ keys }, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
@@ -625,7 +627,8 @@ describe("Secrets", function() {
encrypted: {
key_id: await encryptAES(
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
secretStorageKeys.key_id, "m.megolm_backup.v1",
secretStorageKeys.key_id,
"m.megolm_backup.v1",
),
},
},
@@ -642,32 +645,44 @@ describe("Secrets", function() {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
self_signing: sign<ICrossSigningKey>(
{
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
XSK,
"@alice:example.com",
),
user_signing: sign<ICrossSigningKey>(
{
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
},
}, XSK, "@alice:example.com"),
XSK,
"@alice:example.com",
),
},
});
alice.getKeyBackupVersion = async () => {
return {
version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
}, XSK, "@alice:example.com"),
auth_data: sign(
{
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
},
XSK,
"@alice:example.com",
),
};
};
alice.setAccountData = async function(name, data) {
alice.setAccountData = async function (name, data) {
const event = new MatrixEvent({
type: name,
content: data,
@@ -681,8 +696,7 @@ describe("Secrets", function() {
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==");
expect(await alice.getSecret("m.megolm_backup.v1")).toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
alice.stopClient();
});
});
@@ -17,15 +17,17 @@ import { MatrixClient } from "../../../../src/client";
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { MatrixEvent } from "../../../../src/models/event";
describe("InRoomChannel tests", function() {
describe("InRoomChannel tests", function () {
const ALICE = "@alice:hs.tld";
const BOB = "@bob:hs.tld";
const MALORY = "@malory:hs.tld";
const client = {
getUserId() { return ALICE; },
getUserId() {
return ALICE;
},
} as unknown as MatrixClient;
it("getEventType only returns .request for a message with a msgtype", function() {
it("getEventType only returns .request for a message with a msgtype", function () {
const invalidEvent = new MatrixEvent({
type: "m.key.verification.request",
});
@@ -34,34 +36,29 @@ describe("InRoomChannel tests", function() {
type: "m.room.message",
content: { msgtype: "m.key.verification.request" },
});
expect(InRoomChannel.getEventType(validEvent)).
toStrictEqual("m.key.verification.request");
expect(InRoomChannel.getEventType(validEvent)).toStrictEqual("m.key.verification.request");
const validFooEvent = new MatrixEvent({ type: "m.foo" });
expect(InRoomChannel.getEventType(validFooEvent)).
toStrictEqual("m.foo");
expect(InRoomChannel.getEventType(validFooEvent)).toStrictEqual("m.foo");
});
it("getEventType should return m.room.message for messages", function() {
it("getEventType should return m.room.message for messages", function () {
const messageEvent = new MatrixEvent({
type: "m.room.message",
content: { msgtype: "m.text" },
});
// XXX: The event type doesn't matter too much, just as long as it's not a verification event
expect(InRoomChannel.getEventType(messageEvent)).
toStrictEqual("m.room.message");
expect(InRoomChannel.getEventType(messageEvent)).toStrictEqual("m.room.message");
});
it("getEventType should return actual type for non-message events", function() {
it("getEventType should return actual type for non-message events", function () {
const event = new MatrixEvent({
type: "m.room.member",
content: { },
content: {},
});
expect(InRoomChannel.getEventType(event)).
toStrictEqual("m.room.member");
expect(InRoomChannel.getEventType(event)).toStrictEqual("m.room.member");
});
it("getOtherPartyUserId should not return anything for a request not " +
"directed at me", function() {
it("getOtherPartyUserId should not return anything for a request not " + "directed at me", function () {
const event = new MatrixEvent({
sender: BOB,
type: "m.room.message",
@@ -70,29 +67,25 @@ describe("InRoomChannel tests", function() {
expect(InRoomChannel.getOtherPartyUserId(event, client)).toStrictEqual(undefined);
});
it("getOtherPartyUserId should not return anything an event that is not of a valid " +
"request type", function() {
it("getOtherPartyUserId should not return anything an event that is not of a valid " + "request type", function () {
// invalid because this should be a room message with msgtype
const invalidRequest = new MatrixEvent({
sender: BOB,
type: "m.key.verification.request",
content: { to: ALICE },
});
expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client))
.toStrictEqual(undefined);
expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client)).toStrictEqual(undefined);
const startEvent = new MatrixEvent({
sender: BOB,
type: "m.key.verification.start",
content: { to: ALICE },
});
expect(InRoomChannel.getOtherPartyUserId(startEvent, client))
.toStrictEqual(undefined);
expect(InRoomChannel.getOtherPartyUserId(startEvent, client)).toStrictEqual(undefined);
const fooEvent = new MatrixEvent({
sender: BOB,
type: "m.foo",
content: { to: ALICE },
});
expect(InRoomChannel.getOtherPartyUserId(fooEvent, client))
.toStrictEqual(undefined);
expect(InRoomChannel.getOtherPartyUserId(fooEvent, client)).toStrictEqual(undefined);
});
});
@@ -19,13 +19,13 @@ import { logger } from "../../../../src/logger";
const Olm = global.Olm;
describe("QR code verification", function() {
describe("QR code verification", function () {
if (!global.Olm) {
logger.warn('Not running device verification tests: libolm not present');
logger.warn("Not running device verification tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return Olm.init();
});
@@ -18,23 +18,23 @@ import "../../../olm-loader";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import { logger } from "../../../../src/logger";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { makeTestClients } from './util';
import { makeTestClients } from "./util";
const Olm = global.Olm;
jest.useFakeTimers();
describe("verification request integration tests with crypto layer", function() {
describe("verification request integration tests with crypto layer", function () {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
logger.warn("Not running device verification unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return Olm.init();
});
it("should request and accept a verification", async function() {
it("should request and accept a verification", async function () {
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
@@ -44,7 +44,7 @@ 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: [],
@@ -66,7 +66,7 @@ describe("verification request integration tests with crypto layer", function()
bobVerifier.endTimer();
});
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
await aliceRequest.waitFor(r => r.started);
await aliceRequest.waitFor((r) => r.started);
const aliceVerifier = aliceRequest.verifier;
expect(aliceVerifier).toBeInstanceOf(SAS);
+117 -150
View File
@@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../../olm-loader";
import { makeTestClients } from './util';
import { makeTestClients } from "./util";
import { MatrixEvent } from "../../../../src/models/event";
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
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 { 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";
@@ -31,43 +31,45 @@ import { TestClient } from "../../../TestClient";
const Olm = global.Olm;
let ALICE_DEVICES;
let BOB_DEVICES;
let ALICE_DEVICES: Record<string, IDevice>;
let BOB_DEVICES: Record<string, IDevice>;
describe("SAS verification", function() {
describe("SAS verification", function () {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
logger.warn("Not running device verification unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return Olm.init();
});
it("should error on an unexpected event", async function() {
it("should error on an unexpected event", async function () {
//channel, baseApis, userId, deviceId, startEvent, request
const request = {
onVerifierCancelled: function() {},
onVerifierCancelled: function () {},
} as VerificationRequest;
const channel = {
send: function() {
send: function () {
return Promise.resolve();
},
} as unknown as IVerificationChannel;
const mockClient = {} as unknown as MatrixClient;
const event = new MatrixEvent({ type: 'test' });
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",
content: {},
}));
sas.handleEvent(
new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
content: {},
}),
);
const spy = jest.fn();
await sas.verify().catch(spy);
expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel(new Error('error'));
sas.cancel(new Error("error"));
});
describe("verification", () => {
@@ -75,7 +77,7 @@ describe("SAS verification", function() {
let bob: TestClient;
let aliceSasEvent: ISasEvent | null;
let bobSasEvent: ISasEvent | null;
let aliceVerifier: Verification<any, any>;
let aliceVerifier: SAS;
let bobPromise: Promise<VerificationBase<any, any>>;
let clearTestClientTimeouts: () => void;
@@ -95,38 +97,34 @@ describe("SAS verification", function() {
ALICE_DEVICES = {
Osborne2: {
user_id: "@alice:example.com",
device_id: "Osborne2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Osborne2": aliceDevice.deviceEd25519Key,
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key,
"ed25519:Osborne2": aliceDevice.deviceEd25519Key!,
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key!,
},
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
known: false,
},
};
BOB_DEVICES = {
Dynabook: {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice.deviceEd25519Key,
"curve25519:Dynabook": bobDevice.deviceCurve25519Key,
"ed25519:Dynabook": bobDevice.deviceEd25519Key!,
"curve25519:Dynabook": bobDevice.deviceCurve25519Key!,
},
verified: DeviceInfo.DeviceVerification.UNVERIFIED,
known: false,
},
};
alice.client.crypto!.deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES);
alice.client.downloadKeys = () => {
return Promise.resolve({});
};
bob.client.crypto!.deviceList.storeDevicesForUser(
"@alice:example.com", ALICE_DEVICES,
);
bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES);
bob.client.downloadKeys = () => {
return Promise.resolve({});
};
@@ -135,8 +133,8 @@ describe("SAS verification", function() {
bobSasEvent = null;
bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier!.on("show_sas", (e) => {
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
@@ -157,8 +155,10 @@ describe("SAS verification", function() {
});
aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!,
);
verificationMethods.SAS,
bob.client.getUserId()!,
bob.deviceId!,
) as SAS;
aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
@@ -177,10 +177,7 @@ describe("SAS verification", function() {
});
});
afterEach(async () => {
await Promise.all([
alice.stop(),
bob.stop(),
]);
await Promise.all([alice.stop(), bob.stop()]);
clearTestClientTimeouts();
});
@@ -189,23 +186,21 @@ describe("SAS verification", function() {
let macMethod;
let keyAgreement;
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = function(type, map) {
bob.client.sendToDevice = function (type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code;
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!]
.key_agreement_protocol;
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!].key_agreement_protocol;
}
return origSendToDevice(type, map);
};
alice.httpBackend.when('POST', '/keys/query').respond(200, {
alice.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when('POST', '/keys/query').respond(200, {
bob.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
@@ -224,11 +219,9 @@ describe("SAS verification", function() {
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
// make sure Alice and Bob verified each other
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice?.isVerified()).toBeTruthy();
});
@@ -244,27 +237,27 @@ 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 = ['hkdf-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!]
.message_authentication_code;
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
}
return bobOrigSendToDevice(type, map);
};
alice.httpBackend.when('POST', '/keys/query').respond(200, {
alice.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when('POST', '/keys/query').respond(200, {
bob.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
@@ -280,11 +273,9 @@ describe("SAS verification", function() {
expect(macMethod).toBe("hkdf-hmac-sha256");
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice!.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice!.isVerified()).toBeTruthy();
});
@@ -300,27 +291,25 @@ 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 = ["hmac-sha256"];
}
return aliceOrigSendToDevice(type, map);
};
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!]
.message_authentication_code;
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
}
return bobOrigSendToDevice(type, map);
};
alice.httpBackend.when('POST', '/keys/query').respond(200, {
alice.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when('POST', '/keys/query').respond(200, {
bob.httpBackend.when("POST", "/keys/query").respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
@@ -336,41 +325,33 @@ describe("SAS verification", function() {
expect(macMethod).toBe("hmac-sha256");
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice?.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice?.isVerified()).toBeTruthy();
});
it("should verify a cross-signing key", async () => {
alice.httpBackend.when('POST', '/keys/device_signing/upload').respond(
200, {},
);
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
alice.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {});
alice.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
alice.httpBackend.flush(undefined, 2);
await resetCrossSigningKeys(alice.client);
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
bob.httpBackend.when("POST", "/keys/device_signing/upload").respond(200, {});
bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
bob.httpBackend.flush(undefined, 2);
await resetCrossSigningKeys(bob.client);
bob.client.crypto!.deviceList.storeCrossSigningForUser(
"@alice:example.com", {
keys: alice.client.crypto!.crossSigningInfo.keys,
crossSigningVerifiedBefore: false,
firstUse: true,
},
);
bob.client.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: alice.client.crypto!.crossSigningInfo.keys,
crossSigningVerifiedBefore: false,
firstUse: true,
});
const verifyProm = Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => {
bob.httpBackend.when(
'POST', '/keys/signatures/upload',
).respond(200, {});
bob.httpBackend.when("POST", "/keys/signatures/upload").respond(200, {});
bob.httpBackend.flush(undefined, 1, 2000);
return verifier.verify();
}),
@@ -378,9 +359,7 @@ describe("SAS verification", function() {
await verifyProm;
const bobDeviceTrust = alice.client.checkDeviceTrust(
"@bob:example.com", "Dynabook",
);
const bobDeviceTrust = alice.client.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();
@@ -388,15 +367,13 @@ describe("SAS verification", function() {
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
expect(aliceTrust.isTofu()).toBeTruthy();
const aliceDeviceTrust = bob.client.checkDeviceTrust(
"@alice:example.com", "Osborne2",
);
const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2");
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy();
});
});
it("should send a cancellation message on error", async function() {
it("should send a cancellation message on error", async function () {
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
@@ -412,8 +389,8 @@ describe("SAS verification", function() {
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier!.on("show_sas", (e) => {
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
(<SAS>request.verifier!).on(SasEvent.ShowSas, (e) => {
e.mismatch();
});
resolve(request.verifier!);
@@ -421,7 +398,9 @@ describe("SAS verification", function() {
});
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();
@@ -432,26 +411,24 @@ describe("SAS verification", function() {
]);
expect(aliceSpy).toHaveBeenCalled();
expect(bobSpy).toHaveBeenCalled();
expect(alice.client.setDeviceVerified)
.not.toHaveBeenCalled();
expect(bob.client.setDeviceVerified)
.not.toHaveBeenCalled();
expect(alice.client.setDeviceVerified).not.toHaveBeenCalled();
expect(bob.client.setDeviceVerified).not.toHaveBeenCalled();
alice.stop();
bob.stop();
clearTestClientTimeouts();
});
describe("verification in DM", function() {
let alice;
let bob;
let aliceSasEvent;
let bobSasEvent;
let aliceVerifier;
let bobPromise;
let clearTestClientTimeouts;
describe("verification in DM", function () {
let alice: TestClient;
let bob: TestClient;
let aliceSasEvent: ISasEvent | null;
let bobSasEvent: ISasEvent | null;
let aliceVerifier: SAS;
let bobPromise: Promise<void>;
let clearTestClientTimeouts: Function;
beforeEach(async function() {
beforeEach(async function () {
[[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
{ userId: "@alice:example.com", deviceId: "Osborne2" },
@@ -477,7 +454,7 @@ describe("SAS verification", function() {
);
};
alice.client.downloadKeys = () => {
return Promise.resolve();
return Promise.resolve({});
};
bob.client.crypto!.setDeviceVerification = jest.fn();
@@ -495,16 +472,16 @@ describe("SAS verification", function() {
return "bob+base64+ed25519+key";
};
bob.client.downloadKeys = () => {
return Promise.resolve();
return Promise.resolve({});
};
aliceSasEvent = null;
bobSasEvent = null;
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) => {
bob.client.on(CryptoEvent.VerificationRequest, async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME) as SAS;
verifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
@@ -525,12 +502,10 @@ describe("SAS verification", function() {
});
});
const aliceRequest = await alice.client.requestVerificationDM(
bob.client.getUserId(), "!room_id",
);
await aliceRequest.waitFor(r => r.started);
aliceVerifier = aliceRequest.verifier;
aliceVerifier.on("show_sas", (e) => {
const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id");
await aliceRequest.waitFor((r) => r.started);
aliceVerifier = aliceRequest.verifier! as SAS;
aliceVerifier.on(SasEvent.ShowSas, (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
@@ -547,40 +522,32 @@ describe("SAS verification", function() {
}
});
});
afterEach(async function() {
await Promise.all([
alice.stop(),
bob.stop(),
]);
afterEach(async function () {
await Promise.all([alice.stop(), bob.stop()]);
clearTestClientTimeouts();
});
it("should verify a key", async function() {
await Promise.all([
aliceVerifier.verify(),
bobPromise,
]);
it("should verify a key", async function () {
await Promise.all([aliceVerifier.verify(), bobPromise]);
// make sure Alice and Bob verified each other
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" },
);
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,28 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../../olm-loader';
import { MatrixClient, MatrixEvent } from '../../../../src/matrix';
import "../../../olm-loader";
import { MatrixClient, MatrixEvent } from "../../../../src/matrix";
import { encodeBase64 } from "../../../../src/crypto/olmlib";
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';
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();
// Private key for tests only
const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, 0x05,
0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("self-verifications", () => {
beforeAll(function() {
beforeAll(function () {
return global.Olm.init();
});
@@ -47,26 +45,22 @@ describe("self-verifications", () => {
storeCrossSigningKeyCache: jest.fn(),
};
const crossSigningInfo = new CrossSigningInfo(
userId,
{},
cacheCallbacks,
);
const crossSigningInfo = new CrossSigningInfo(userId, {}, cacheCallbacks);
crossSigningInfo.keys = {
master: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
user_id: "user-id",
},
self_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
user_id: "user-id",
},
user_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
user_id: "user-id",
},
};
@@ -114,18 +108,15 @@ describe("self-verifications", () => {
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3);
expect(secretStorage.request.mock.calls.length).toBe(4);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
.toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
.toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1]).toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1]).toEqual(testKey);
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
.toEqual(testKey);
expect(storeSessionBackupPrivateKey.mock.calls[0][0]).toEqual(testKey);
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
expect(result).toBeInstanceOf(Array);
expect(result[0][0]).toBe(testKeyPub);
expect(result[1][0]).toBe(testKeyPub);
expect(result![0][0]).toBe(testKeyPub);
expect(result![1][0]).toBe(testKeyPub);
});
});
+51 -40
View File
@@ -15,43 +15,51 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event";
import { TestClient } from "../../../TestClient";
import { IContent, MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
import { Room, RoomEvent } from "../../../../src/models/room";
import { logger } from '../../../../src/logger';
import { MatrixClient, ClientEvent } from '../../../../src/client';
import { logger } from "../../../../src/logger";
import { MatrixClient, ClientEvent, ICreateClientOpts } from "../../../../src/client";
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
interface UserInfo {
userId: string;
deviceId: string;
}
export async function makeTestClients(
userInfos: UserInfo[],
options: Partial<ICreateClientOpts>,
): 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: matrixClient.getUserId()!,
type: type,
content: msg,
});
const client = clientMap[userId][deviceId];
const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client.crypto!) :
Promise.resolve();
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: matrixClient.getUserId()!,
type: type,
content: msg,
});
const client = clientMap[userId][deviceId];
const decryptionPromise = event.isEncrypted()
? event.attemptDecryption(client.crypto!)
: Promise.resolve();
decryptionPromise.then(
() => client.emit(ClientEvent.ToDeviceEvent, event),
);
decryptionPromise.then(() => client.emit(ClientEvent.ToDeviceEvent, event));
}
}
}
}
}
return {};
};
const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => {
return {};
};
const makeSendEvent = (matrixClient: MatrixClient) => (room: string, type: string, content: IContent) => {
// make up a unique ID as the event ID
const eventId = "$" + matrixClient.makeTxnId();
const rawEvent = {
@@ -63,15 +71,17 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
origin_server_ts: Date.now(),
};
const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
unsigned: {
transaction_id: matrixClient.makeTxnId(),
},
}));
const remoteEcho = new MatrixEvent(
Object.assign({}, rawEvent, {
unsigned: {
transaction_id: matrixClient.makeTxnId(),
},
}),
);
const timeout = setTimeout(() => {
for (const tc of clients) {
const room = new Room('test', tc.client, tc.client.getUserId()!);
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!!");
@@ -88,22 +98,23 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
};
for (const userInfo of userInfos) {
let keys = {};
let keys: Record<string, Uint8Array> = {};
if (!options) options = {};
if (!options.cryptoCallbacks) options.cryptoCallbacks = {};
if (!options.cryptoCallbacks.saveCrossSigningKeys) {
options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; };
options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ];
options.cryptoCallbacks.saveCrossSigningKeys = (k) => {
keys = k;
};
// @ts-ignore tsc getting confused by overloads
options.cryptoCallbacks.getCrossSigningKey = (typ) => keys[typ];
}
const testClient = new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined,
options,
);
const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
if (!(userInfo.userId in clientMap)) {
clientMap[userInfo.userId] = {};
}
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
testClient.client.sendToDevice = makeSendToDevice(testClient.client);
// @ts-ignore tsc getting confused by overloads
testClient.client.sendEvent = makeSendEvent(testClient.client);
clients.push(testClient);
}
@@ -13,12 +13,15 @@ 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 { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from
"../../../../src/crypto/verification/request/VerificationRequest";
import {
VerificationRequest,
READY_TYPE,
START_TYPE,
DONE_TYPE,
} from "../../../../src/crypto/verification/request/VerificationRequest";
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { ToDeviceChannel } from
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event";
import { ToDeviceChannel } from "../../../../src/crypto/verification/request/ToDeviceChannel";
import { IContent, MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
@@ -30,26 +33,32 @@ type MockClient = MatrixClient & {
function makeMockClient(userId: string, deviceId: string): MockClient {
let counter = 1;
let events: MatrixEvent[] = [];
const deviceEvents = {};
const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
return {
getUserId() { return userId; },
getDeviceId() { return deviceId; },
getUserId() {
return userId;
},
getDeviceId() {
return deviceId;
},
sendEvent(roomId, type, content) {
sendEvent(roomId: string, type: string, content: IContent) {
counter = counter + 1;
const eventId = `$${userId}-${deviceId}-${counter}`;
events.push(new MatrixEvent({
sender: userId,
event_id: eventId,
room_id: roomId,
type,
content,
origin_server_ts: Date.now(),
}));
events.push(
new MatrixEvent({
sender: userId,
event_id: eventId,
room_id: roomId,
type,
content,
origin_server_ts: Date.now(),
}),
);
return Promise.resolve({ event_id: eventId });
},
sendToDevice(type, msgMap) {
sendToDevice(type: string, msgMap: Record<string, Record<string, IContent>>) {
for (const userId of Object.keys(msgMap)) {
const deviceMap = msgMap[userId];
for (const deviceId of Object.keys(deviceMap)) {
@@ -84,7 +93,7 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
}
const MOCK_METHOD = "mock-verify";
class MockVerifier extends VerificationBase<'', any> {
class MockVerifier extends VerificationBase<"", any> {
public _channel;
public _startEvent;
constructor(
@@ -111,7 +120,7 @@ class MockVerifier extends VerificationBase<'', any> {
}
}
async handleEvent(event) {
async handleEvent(event: MatrixEvent) {
if (event.getType() === DONE_TYPE && !this._startEvent) {
await this._channel.send(DONE_TYPE, {});
}
@@ -122,12 +131,14 @@ class MockVerifier extends VerificationBase<'', any> {
}
}
function makeRemoteEcho(event) {
return new MatrixEvent(Object.assign({}, event.event, {
unsigned: {
transaction_id: "abc",
},
}));
function makeRemoteEcho(event: MatrixEvent) {
return new MatrixEvent(
Object.assign({}, event.event, {
unsigned: {
transaction_id: "abc",
},
}),
);
}
async function distributeEvent(
@@ -135,33 +146,26 @@ async function distributeEvent(
theirRequest: VerificationRequest,
event: MatrixEvent,
): Promise<void> {
await ownRequest.channel.handleEvent(
makeRemoteEcho(event),
ownRequest,
true,
);
await ownRequest.channel.handleEvent(makeRemoteEcho(event), ownRequest, true);
await theirRequest.channel.handleEvent(event, theirRequest, true);
}
jest.useFakeTimers();
describe("verification request unit tests", function() {
it("transition from UNSENT to DONE through happy path", async function() {
describe("verification request unit tests", function () {
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 verificationMethods = new Map([[MOCK_METHOD, MockVerifier]]) as unknown as Map<
string,
typeof VerificationBase
>;
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()!),
verificationMethods,
alice,
);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
verificationMethods,
bob,
);
const bobRequest = new VerificationRequest(new InRoomChannel(bob, "!room"), verificationMethods, bob);
expect(aliceRequest.invalid).toBe(true);
expect(bobRequest.invalid).toBe(true);
@@ -199,23 +203,23 @@ describe("verification request unit tests", function() {
expect(bobRequest.done).toBe(true);
});
it("methods only contains common methods", async 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 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()!),
aliceVerificationMethods, alice);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
bobVerificationMethods,
bob,
aliceVerificationMethods,
alice,
);
const bobRequest = new VerificationRequest(new InRoomChannel(bob, "!room"), bobVerificationMethods, bob);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await distributeEvent(aliceRequest, bobRequest, requestEvent);
@@ -226,7 +230,7 @@ describe("verification request unit tests", function() {
expect(bobRequest.methods).toStrictEqual(["c"]);
});
it("other client accepting request puts it in observeOnly mode", async function() {
it("other client accepting request puts it in observeOnly mode", async function () {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
@@ -237,16 +241,8 @@ describe("verification request unit tests", function() {
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bob1Request = new VerificationRequest(
new InRoomChannel(bob1, "!room"),
new Map(),
bob1,
);
const bob2Request = new VerificationRequest(
new InRoomChannel(bob2, "!room"),
new Map(),
bob2,
);
const bob1Request = new VerificationRequest(new InRoomChannel(bob1, "!room"), new Map(), bob1);
const bob2Request = new VerificationRequest(new InRoomChannel(bob2, "!room"), new Map(), bob2);
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
@@ -258,12 +254,13 @@ describe("verification request unit tests", function() {
expect(bob2Request.observeOnly).toBe(true);
});
it("verify own device with to_device messages", async 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 verificationMethods = new Map([[MOCK_METHOD, MockVerifier]]) as unknown as Map<
string,
typeof VerificationBase
>;
const bob1Request = new VerificationRequest(
new ToDeviceChannel(
bob1,
@@ -300,7 +297,7 @@ describe("verification request unit tests", function() {
expect(bob2Request.done).toBe(true);
});
it("request times out after 10 minutes", async function() {
it("request times out after 10 minutes", async function () {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
@@ -318,7 +315,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest._cancellingUserId).toBe(alice.getUserId());
});
it("request times out 2 minutes after receipt", async function() {
it("request times out 2 minutes after receipt", async function () {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
@@ -328,11 +325,7 @@ describe("verification request unit tests", function() {
);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map(),
bob,
);
const bobRequest = new VerificationRequest(new InRoomChannel(bob, "!room"), new Map(), bob);
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
+24 -20
View File
@@ -23,13 +23,7 @@ limitations under the License.
// 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 { WidgetApi, WidgetApiToWidgetAction, MatrixCapabilities, ITurnServer, IRoomEvent } from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
@@ -88,7 +82,9 @@ describe("RoomWidgetClient", () => {
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",
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
);
});
@@ -105,8 +101,8 @@ describe("RoomWidgetClient", () => {
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));
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 } }),
@@ -118,7 +114,12 @@ describe("RoomWidgetClient", () => {
// 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]);
expect(
room!
.getLiveTimeline()
.getEvents()
.map((e) => e.getEffectiveEvent()),
).toEqual([event]);
});
});
@@ -157,7 +158,10 @@ describe("RoomWidgetClient", () => {
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",
"org.example.foo",
"bar",
{ hello: "world" },
"!1:example.org",
);
});
@@ -166,8 +170,8 @@ describe("RoomWidgetClient", () => {
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));
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 } }),
@@ -179,7 +183,7 @@ describe("RoomWidgetClient", () => {
// 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);
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});
it("backfills", async () => {
@@ -195,7 +199,7 @@ describe("RoomWidgetClient", () => {
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event);
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});
});
@@ -260,8 +264,8 @@ describe("RoomWidgetClient", () => {
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));
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 } }),
@@ -308,7 +312,7 @@ describe("RoomWidgetClient", () => {
};
let emitServer2: () => void;
const getServer2 = new Promise<ITurnServer>(resolve => emitServer2 = () => resolve(server2));
const getServer2 = new Promise<ITurnServer>((resolve) => (emitServer2 = () => resolve(server2)));
widgetApi.getTurnServers.mockImplementation(async function* () {
yield server1;
yield await getServer2;
@@ -321,7 +325,7 @@ describe("RoomWidgetClient", () => {
expect(client.getTurnServers()).toEqual([clientServer1]);
// Subsequent servers arrive asynchronously and should emit an event
const emittedServer = new Promise<IClientTurnServer[]>(resolve =>
const emittedServer = new Promise<IClientTurnServer[]>((resolve) =>
client.once(ClientEvent.TurnServers, resolve),
);
emitServer2!();
+3 -3
View File
@@ -18,7 +18,7 @@ import { MatrixClient, MatrixEvent, MatrixEventEvent, MatrixScheduler, Room } fr
import { eventMapperFor } from "../../src/event-mapper";
import { IStore } from "../../src/store";
describe("eventMapperFor", function() {
describe("eventMapperFor", function () {
let rooms: Room[] = [];
const userId = "@test:example.org";
@@ -29,10 +29,10 @@ describe("eventMapperFor", function() {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: function() {} as any, // NOP
fetchFn: function () {} as any, // NOP
store: {
getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId) ?? null;
return rooms.find((r) => r.roomId === roomId) ?? null;
},
} as IStore,
scheduler: {
+151 -80
View File
@@ -24,13 +24,16 @@ import {
MatrixClient,
MatrixEvent,
MatrixEventEvent,
RelationType,
Room,
} from '../../src';
import { Thread } from "../../src/models/thread";
RoomEvent,
} from "../../src";
import { FeatureSupport, Thread } from "../../src/models/thread";
import { ReEmitter } from "../../src/ReEmitter";
import { eventMapperFor } from "../../src/event-mapper";
describe('EventTimelineSet', () => {
const roomId = '!foo:bar';
describe("EventTimelineSet", () => {
const roomId = "!foo:bar";
const userA = "@alice:bar";
let room: Room;
@@ -42,7 +45,7 @@ describe('EventTimelineSet', () => {
let replyEvent: MatrixEvent;
const itShouldReturnTheRelatedEvents = () => {
it('should return the related events', () => {
it("should return the related events", () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId()!,
@@ -55,45 +58,49 @@ describe('EventTimelineSet', () => {
});
};
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(),
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",
},
},
"rel_type": "m.thread",
},
},
}, room.client);
room.client,
);
beforeEach(() => {
client = utils.mock(MatrixClient, 'MatrixClient');
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter');
client = utils.mock(MatrixClient, "MatrixClient");
client.reEmitter = utils.mock(ReEmitter, "ReEmitter");
room = new Room(roomId, client, userA);
eventTimelineSet = new EventTimelineSet(room);
eventTimeline = new EventTimeline(eventTimelineSet);
messageEvent = utils.mkMessage({
room: roomId,
user: userA,
msg: 'Hi!',
msg: "Hi!",
event: true,
});
replyEvent = utils.mkReplyMessage({
room: roomId,
user: userA,
msg: 'Hoo!',
msg: "Hoo!",
event: true,
replyToMessage: messageEvent,
});
});
describe('addLiveEvent', () => {
describe("addLiveEvent", () => {
it("Adds event to the live timeline in the timeline set", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
@@ -111,7 +118,10 @@ describe('EventTimelineSet', () => {
// make a duplicate
const duplicateMessageEvent = utils.mkMessage({
room: roomId, user: userA, msg: "dupe", event: true,
room: roomId,
user: userA,
msg: "dupe",
event: true,
});
duplicateMessageEvent.event.event_id = messageEvent.getId();
@@ -133,7 +143,7 @@ describe('EventTimelineSet', () => {
});
});
describe('addEventToTimeline', () => {
describe("addEventToTimeline", () => {
let thread: Thread;
beforeEach(() => {
@@ -153,19 +163,10 @@ describe('EventTimelineSet', () => {
it("Make sure legacy overload passing options directly as parameters still works", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(() => {
eventTimelineSet.addEventToTimeline(
messageEvent,
liveTimeline,
true,
);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true);
}).not.toThrow();
expect(() => {
eventTimelineSet.addEventToTimeline(
messageEvent,
liveTimeline,
true,
false,
);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false);
}).not.toThrow();
});
@@ -204,8 +205,90 @@ describe('EventTimelineSet', () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
describe('non-room timeline', () => {
it('Adds event to timeline', () => {
it("should allow edits to be added to thread timeline", async () => {
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
Thread.hasServerSideSupport = FeatureSupport.Stable;
const sender = "@alice:matrix.org";
const root = utils.mkEvent({
event: true,
content: {
body: "Thread root",
},
type: EventType.RoomMessage,
sender,
});
room.addLiveEvents([root]);
const threadReply = utils.mkEvent({
event: true,
content: {
"body": "Thread reply",
"m.relates_to": {
event_id: root.getId()!,
rel_type: RelationType.Thread,
},
},
type: EventType.RoomMessage,
sender,
});
root.setUnsigned({
"m.relations": {
[RelationType.Thread]: {
count: 1,
latest_event: {
content: threadReply.getContent(),
origin_server_ts: 5,
room_id: room.roomId,
sender,
type: EventType.RoomMessage,
event_id: threadReply.getId()!,
user_id: sender,
age: 1,
},
current_user_participated: true,
},
},
});
const editToThreadReply = utils.mkEvent({
event: true,
content: {
"body": " * edit",
"m.new_content": {
"body": "edit",
"msgtype": "m.text",
"org.matrix.msc1767.text": "edit",
},
"m.relates_to": {
event_id: threadReply.getId()!,
rel_type: RelationType.Replace,
},
},
type: EventType.RoomMessage,
sender,
});
jest.spyOn(client, "paginateEventTimeline").mockImplementation(async () => {
thread.timelineSet.getLiveTimeline().addEvent(threadReply, { toStartOfTimeline: true });
return true;
});
jest.spyOn(client, "relations").mockResolvedValue({
events: [],
});
const thread = room.createThread(root.getId()!, root, [threadReply, editToThreadReply], false);
thread.once(RoomEvent.TimelineReset, () => {
const lastEvent = thread.timeline.at(-1)!;
expect(lastEvent.getContent().body).toBe(" * edit");
});
});
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
@@ -222,24 +305,16 @@ describe('EventTimelineSet', () => {
});
});
describe('aggregateRelations', () => {
describe('with unencrypted events', () => {
describe("aggregateRelations", () => {
describe("with unencrypted events", () => {
beforeEach(() => {
eventTimelineSet.addEventsToTimeline(
[
messageEvent,
replyEvent,
],
true,
eventTimeline,
'foo',
);
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
});
itShouldReturnTheRelatedEvents();
});
describe('with events to be decrypted', () => {
describe("with events to be decrypted", () => {
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
@@ -247,26 +322,18 @@ describe('EventTimelineSet', () => {
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
beforeEach(() => {
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, 'shouldAttemptDecryption');
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption");
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, 'shouldAttemptDecryption');
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, "shouldAttemptDecryption");
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, 'isDecryptionFailure');
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
eventTimelineSet.addEventsToTimeline(
[
messageEvent,
replyEvent,
],
true,
eventTimeline,
'foo',
);
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
});
it('should not return the related events', () => {
it("should not return the related events", () => {
eventTimelineSet.relations.aggregateChildEvent(messageEvent);
const relations = eventTimelineSet.relations.getChildEventsForEvent(
messageEvent.getId()!,
@@ -276,7 +343,7 @@ describe('EventTimelineSet', () => {
expect(relations).toBeUndefined();
});
describe('after decryption', () => {
describe("after decryption", () => {
beforeEach(() => {
// simulate decryption failure once
messageEventIsDecryptionFailureSpy.mockReturnValue(true);
@@ -302,22 +369,26 @@ describe('EventTimelineSet', () => {
});
describe("canContain", () => {
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()!,
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",
},
},
"rel_type": "m.thread",
},
},
}, room.client);
room.client,
);
let thread: Thread;
+210 -167
View File
@@ -1,4 +1,4 @@
import { mocked } from 'jest-mock';
import { mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils";
import { Direction, EventTimeline } from "../../src/models/event-timeline";
@@ -8,7 +8,7 @@ import { Room } from "../../src/models/room";
import { RoomMember } from "../../src/models/room-member";
import { EventTimelineSet } from "../../src/models/event-timeline-set";
describe("EventTimeline", function() {
describe("EventTimeline", function () {
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
@@ -19,7 +19,7 @@ describe("EventTimeline", function() {
const getTimeline = (): EventTimeline => {
const room = new Room(roomId, mockClient, userA);
const timelineSet = new EventTimelineSet(room);
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
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
@@ -31,29 +31,34 @@ describe("EventTimeline", function() {
return timeline;
};
beforeEach(function() {
beforeEach(function () {
// reset any RoomState mocks
jest.resetAllMocks();
timeline = getTimeline();
});
describe("construction", function() {
it("getRoomId should get room id", function() {
describe("construction", function () {
it("getRoomId should get room id", function () {
const v = timeline.getRoomId();
expect(v).toEqual(roomId);
});
});
describe("initialiseState", function() {
it("should copy state events to start and end state", function() {
describe("initialiseState", function () {
it("should copy state events to start and end state", function () {
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
room: roomId,
mship: "invite",
user: userB,
skey: userA,
event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
type: "m.room.name",
room: roomId,
user: userB,
event: true,
content: { name: "New room" },
}),
@@ -61,49 +66,51 @@ describe("EventTimeline", function() {
timeline.initialiseState(events);
// @ts-ignore private prop
const timelineStartState = timeline.startState!;
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(
events,
{ timelineWasEmpty: undefined },
);
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
timelineWasEmpty: undefined,
});
// @ts-ignore private prop
const timelineEndState = timeline.endState!;
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(
events,
{ timelineWasEmpty: undefined },
);
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
timelineWasEmpty: undefined,
});
});
it("should raise an exception if called after events are added", function() {
const event =
utils.mkMessage({
room: roomId, user: userA, msg: "Adam stole the plushies",
event: true,
});
it("should raise an exception if called after events are added", function () {
const event = utils.mkMessage({
room: roomId,
user: userA,
msg: "Adam stole the plushies",
event: true,
});
const state = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
room: roomId,
mship: "invite",
user: userB,
skey: userA,
event: true,
}),
];
expect(function() {
expect(function () {
timeline.initialiseState(state);
}).not.toThrow();
timeline.addEvent(event, { toStartOfTimeline: false });
expect(function() {
expect(function () {
timeline.initialiseState(state);
}).toThrow();
});
});
describe("paginationTokens", function() {
it("pagination tokens should start null", function() {
describe("paginationTokens", function () {
it("pagination tokens should start null", function () {
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null);
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");
@@ -121,13 +128,13 @@ describe("EventTimeline", function() {
});
});
describe("neighbouringTimelines", function() {
it("neighbouring timelines should start null", function() {
describe("neighbouringTimelines", function () {
it("neighbouring timelines should start null", function () {
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(null);
});
it("setNeighbouringTimeline should set neighbour", function() {
it("setNeighbouringTimeline should set neighbour", function () {
const prev = getTimeline();
const next = getTimeline();
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
@@ -136,42 +143,44 @@ describe("EventTimeline", function() {
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
});
it("setNeighbouringTimeline should throw if called twice", function() {
it("setNeighbouringTimeline should throw if called twice", function () {
const prev = getTimeline();
const next = getTimeline();
expect(function() {
expect(function () {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(prev);
expect(function() {
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
expect(function () {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).toThrow();
expect(function() {
expect(function () {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(next);
expect(function() {
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
expect(function () {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).toThrow();
});
});
describe("addEvent", function() {
describe("addEvent", function () {
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
room: roomId,
user: userA,
msg: "hungry hungry hungry",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom",
room: roomId,
user: userB,
msg: "nom nom nom",
event: true,
}),
];
it("should be able to add events to the end", function() {
it("should be able to add events to the end", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
@@ -181,7 +190,7 @@ describe("EventTimeline", function() {
expect(timeline.getEvents()[1]).toEqual(events[1]);
});
it("should be able to add events to the start", function() {
it("should be able to add events to the start", function () {
timeline.addEvent(events[0], { toStartOfTimeline: true });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: true });
@@ -191,7 +200,7 @@ describe("EventTimeline", function() {
expect(timeline.getEvents()[1]).toEqual(events[0]);
});
it("should set event.sender for new and old events", function() {
it("should set event.sender for new and old events", function () {
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
@@ -200,27 +209,31 @@ describe("EventTimeline", function() {
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;
});
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.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
type: "m.room.name",
room: roomId,
user: userA,
event: true,
content: { name: "New Room Name" },
});
const oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
type: "m.room.name",
room: roomId,
user: userA,
event: true,
content: { name: "Old Room Name" },
});
@@ -230,128 +243,159 @@ describe("EventTimeline", function() {
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
it("should set event.target for new and old m.room.member events", function () {
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";
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);
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;
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room",
},
}),
];
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
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).
not.toHaveBeenCalled();
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);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room",
},
}),
];
it(
"should call setStateEvents on the right RoomState with the right " + "forwardLooking value for new events",
function () {
const events = [
utils.mkMembership({
room: roomId,
mship: "invite",
user: userB,
skey: userA,
event: true,
}),
utils.mkEvent({
type: "m.room.name",
room: roomId,
user: userB,
event: true,
content: {
name: "New room",
},
}),
];
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined,
});
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[1]], {
timelineWasEmpty: undefined,
});
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
not.toHaveBeenCalled();
});
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).not.toHaveBeenCalled();
},
);
it(
"should call setStateEvents on the right RoomState with the right " + "forwardLooking value for old events",
function () {
const events = [
utils.mkMembership({
room: roomId,
mship: "invite",
user: userB,
skey: userA,
event: true,
}),
utils.mkEvent({
type: "m.room.name",
room: roomId,
user: userB,
event: true,
content: {
name: "New room",
},
}),
];
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true });
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined,
});
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).not.toHaveBeenCalled();
},
);
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).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();
expect(() =>
timeline.addEvent(events[0], { toStartOfTimeline: false, roomState: new RoomState(roomId) }),
).not.toThrow();
});
});
describe("removeEvent", function() {
describe("removeEvent", function () {
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
room: roomId,
user: userA,
msg: "hungry hungry hungry",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom",
room: roomId,
user: userB,
msg: "nom nom nom",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "piiie",
room: roomId,
user: userB,
msg: "piiie",
event: true,
}),
];
it("should remove events", function() {
it("should remove events", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getEvents().length).toEqual(2);
@@ -365,7 +409,7 @@ describe("EventTimeline", function() {
expect(timeline.getEvents().length).toEqual(0);
});
it("should update baseIndex", function() {
it("should update baseIndex", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[2], { toStartOfTimeline: false });
@@ -384,15 +428,14 @@ describe("EventTimeline", function() {
// this is basically https://github.com/vector-im/vector-web/issues/937
// - 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);
});
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);
});
});
});
@@ -0,0 +1,41 @@
/*
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ExtensibleEventType, IPartialEvent } from "../../../src/@types/extensible_events";
import { ExtensibleEvent } from "../../../src/extensible_events_v1/ExtensibleEvent";
class MockEvent extends ExtensibleEvent<any> {
public constructor(wireEvent: IPartialEvent<any>) {
super(wireEvent);
}
public serialize(): IPartialEvent<object> {
throw new Error("Not implemented for tests");
}
public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
throw new Error("Not implemented for tests");
}
}
describe("ExtensibleEvent", () => {
it("should expose the wire event directly", () => {
const input: IPartialEvent<any> = { type: "org.example.custom", content: { hello: "world" } };
const event = new MockEvent(input);
expect(event.wireFormat).toBe(input);
expect(event.wireContent).toBe(input.content);
});
});
@@ -0,0 +1,156 @@
/*
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
ExtensibleAnyMessageEventContent,
IPartialEvent,
M_HTML,
M_MESSAGE,
M_TEXT,
} from "../../../src/@types/extensible_events";
import { MessageEvent } from "../../../src/extensible_events_v1/MessageEvent";
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
describe("MessageEvent", () => {
it("should parse m.text", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_TEXT.name]: "Text here",
},
};
const message = new MessageEvent(input);
expect(message.text).toBe("Text here");
expect(message.html).toBeFalsy();
expect(message.renderings.length).toBe(1);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
});
it("should parse m.html", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_TEXT.name]: "Text here",
[M_HTML.name]: "HTML here",
},
};
const message = new MessageEvent(input);
expect(message.text).toBe("Text here");
expect(message.html).toBe("HTML here");
expect(message.renderings.length).toBe(2);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
});
it("should parse m.message", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_MESSAGE.name]: [
{ body: "Text here", mimetype: "text/plain" },
{ body: "HTML here", mimetype: "text/html" },
{ body: "MD here", mimetype: "text/markdown" },
],
// These should be ignored
[M_TEXT.name]: "WRONG Text here",
[M_HTML.name]: "WRONG HTML here",
},
};
const message = new MessageEvent(input);
expect(message.text).toBe("Text here");
expect(message.html).toBe("HTML here");
expect(message.renderings.length).toBe(3);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
expect(message.renderings.some((r) => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true);
});
it("should fail to parse missing text", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
hello: "world",
} as any, // force invalid type
};
expect(() => new MessageEvent(input)).toThrow(
new InvalidEventError("Missing textual representation for event"),
);
});
it("should fail to parse missing plain text in m.message", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_MESSAGE.name]: [{ body: "HTML here", mimetype: "text/html" }],
},
};
expect(() => new MessageEvent(input)).toThrow(
new InvalidEventError("m.message is missing a plain text representation"),
);
});
it("should fail to parse non-array m.message", () => {
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
type: "org.example.message-like",
content: {
[M_MESSAGE.name]: "invalid",
} as any, // force invalid type
};
expect(() => new MessageEvent(input)).toThrow(new InvalidEventError("m.message contents must be an array"));
});
describe("from & serialize", () => {
it("should serialize to a legacy fallback", () => {
const message = MessageEvent.from("Text here", "HTML here");
expect(message.text).toBe("Text here");
expect(message.html).toBe("HTML here");
expect(message.renderings.length).toBe(2);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
const serialized = message.serialize();
expect(serialized.type).toBe("m.room.message");
expect(serialized.content).toMatchObject({
[M_MESSAGE.name]: [
{ body: "Text here", mimetype: "text/plain" },
{ body: "HTML here", mimetype: "text/html" },
],
body: "Text here",
msgtype: "m.text",
format: "org.matrix.custom.html",
formatted_body: "HTML here",
});
});
it("should serialize non-html content to a legacy fallback", () => {
const message = MessageEvent.from("Text here");
expect(message.text).toBe("Text here");
expect(message.renderings.length).toBe(1);
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
const serialized = message.serialize();
expect(serialized.type).toBe("m.room.message");
expect(serialized.content).toMatchObject({
[M_TEXT.name]: "Text here",
body: "Text here",
msgtype: "m.text",
format: undefined,
formatted_body: undefined,
});
});
});
});
@@ -0,0 +1,107 @@
/*
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { PollEndEventContent, M_POLL_END } from "../../../src/@types/polls";
import { IPartialEvent, REFERENCE_RELATION, M_TEXT } from "../../../src/@types/extensible_events";
import { PollEndEvent } from "../../../src/extensible_events_v1/PollEndEvent";
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
describe("PollEndEvent", () => {
// Note: throughout these tests we don't really bother testing that
// MessageEvent is doing its job. It has its own tests to worry about.
it("should parse a poll closure", () => {
const input: IPartialEvent<PollEndEventContent> = {
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_END.name]: {},
[M_TEXT.name]: "Poll closed",
},
};
const event = new PollEndEvent(input);
expect(event.pollEventId).toBe("$poll");
expect(event.closingMessage.text).toBe("Poll closed");
});
it("should fail to parse a missing relationship", () => {
const input: IPartialEvent<PollEndEventContent> = {
type: M_POLL_END.name,
content: {
[M_POLL_END.name]: {},
[M_TEXT.name]: "Poll closed",
} as any, // force invalid type
};
expect(() => new PollEndEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
it("should fail to parse a missing relationship event ID", () => {
const input: IPartialEvent<PollEndEventContent> = {
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
},
[M_POLL_END.name]: {},
[M_TEXT.name]: "Poll closed",
} as any, // force invalid type
};
expect(() => new PollEndEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
it("should fail to parse an improper relationship", () => {
const input: IPartialEvent<PollEndEventContent> = {
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: "org.example.not-relationship",
event_id: "$poll",
},
[M_POLL_END.name]: {},
[M_TEXT.name]: "Poll closed",
} as any, // force invalid type
};
expect(() => new PollEndEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
describe("from & serialize", () => {
it("should serialize to a poll end event", () => {
const event = PollEndEvent.from("$poll", "Poll closed");
expect(event.pollEventId).toBe("$poll");
expect(event.closingMessage.text).toBe("Poll closed");
const serialized = event.serialize();
expect(M_POLL_END.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_END.name]: {},
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
});
});
});
});
@@ -0,0 +1,277 @@
/*
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { M_TEXT, IPartialEvent, REFERENCE_RELATION } from "../../../src/@types/extensible_events";
import {
M_POLL_START,
M_POLL_KIND_DISCLOSED,
PollResponseEventContent,
M_POLL_RESPONSE,
} from "../../../src/@types/polls";
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
import { PollResponseEvent } from "../../../src/extensible_events_v1/PollResponseEvent";
const SAMPLE_POLL = new PollStartEvent({
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
{ id: "one", [M_TEXT.name]: "ONE" },
{ id: "two", [M_TEXT.name]: "TWO" },
{ id: "thr", [M_TEXT.name]: "THR" },
],
},
},
});
describe("PollResponseEvent", () => {
it("should parse a poll response", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: ["one"],
},
},
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(false);
expect(response.answerIds).toMatchObject(["one"]);
expect(response.pollEventId).toBe("$poll");
});
it("should fail to parse a missing relationship", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
[M_POLL_RESPONSE.name]: {
answers: ["one"],
},
} as any, // force invalid type
};
expect(() => new PollResponseEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
it("should fail to parse a missing relationship event ID", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
},
[M_POLL_RESPONSE.name]: {
answers: ["one"],
},
} as any, // force invalid type
};
expect(() => new PollResponseEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
it("should fail to parse an improper relationship", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: "org.example.not-relationship",
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: ["one"],
},
} as any, // force invalid type
};
expect(() => new PollResponseEvent(input)).toThrow(
new InvalidEventError("Relationship must be a reference to an event"),
);
});
describe("validateAgainst", () => {
it("should spoil the vote when no answers", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {},
} as any, // force invalid type
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(true);
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(true);
});
it("should spoil the vote when answers are empty", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: [],
},
},
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(true);
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(true);
});
it("should spoil the vote when answers are empty", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: [],
},
},
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(true);
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(true);
});
it("should spoil the vote when answers are not strings", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: [1, 2, 3],
},
} as any, // force invalid type
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(true);
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(true);
});
describe("consumer usage", () => {
it("should spoil the vote when invalid answers are given", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: ["A", "B", "C"],
},
},
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(false); // it won't know better
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(true);
});
it("should truncate answers to the poll max selections", () => {
const input: IPartialEvent<PollResponseEventContent> = {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: ["one", "two", "thr"],
},
},
};
const response = new PollResponseEvent(input);
expect(response.spoiled).toBe(false); // it won't know better
expect(response.answerIds).toMatchObject(["one", "two", "thr"]);
response.validateAgainst(SAMPLE_POLL);
expect(response.spoiled).toBe(false);
expect(response.answerIds).toMatchObject(["one", "two"]);
});
});
});
describe("from & serialize", () => {
it("should serialize to a poll response event", () => {
const response = PollResponseEvent.from(["A", "B", "C"], "$poll");
expect(response.spoiled).toBe(false);
expect(response.answerIds).toMatchObject(["A", "B", "C"]);
expect(response.pollEventId).toBe("$poll");
const serialized = response.serialize();
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: ["A", "B", "C"],
},
});
});
it("should serialize a spoiled vote", () => {
const response = PollResponseEvent.from([], "$poll");
expect(response.spoiled).toBe(true);
expect(response.answerIds).toMatchObject([]);
expect(response.pollEventId).toBe("$poll");
const serialized = response.serialize();
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: "$poll",
},
[M_POLL_RESPONSE.name]: {
answers: undefined,
},
});
});
});
});
@@ -0,0 +1,337 @@
/*
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { M_TEXT, IPartialEvent } from "../../../src/@types/extensible_events";
import {
M_POLL_START,
M_POLL_KIND_DISCLOSED,
PollAnswer,
PollStartEventContent,
M_POLL_KIND_UNDISCLOSED,
} from "../../../src/@types/polls";
import { PollStartEvent, PollAnswerSubevent } from "../../../src/extensible_events_v1/PollStartEvent";
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
describe("PollAnswerSubevent", () => {
// Note: throughout these tests we don't really bother testing that
// MessageEvent is doing its job. It has its own tests to worry about.
it("should parse an answer representation", () => {
const input: IPartialEvent<PollAnswer> = {
type: "org.matrix.sdk.poll.answer",
content: {
id: "one",
[M_TEXT.name]: "ONE",
},
};
const answer = new PollAnswerSubevent(input);
expect(answer.id).toBe("one");
expect(answer.text).toBe("ONE");
});
it("should fail to parse answers without an ID", () => {
const input: IPartialEvent<PollAnswer> = {
type: "org.matrix.sdk.poll.answer",
content: {
[M_TEXT.name]: "ONE",
} as any, // force invalid type
};
expect(() => new PollAnswerSubevent(input)).toThrow(
new InvalidEventError("Answer ID must be a non-empty string"),
);
});
it("should fail to parse answers without text", () => {
const input: IPartialEvent<PollAnswer> = {
type: "org.matrix.sdk.poll.answer",
content: {
id: "one",
} as any, // force invalid type
};
expect(() => new PollAnswerSubevent(input)).toThrow(); // we don't check message - that'll be MessageEvent's problem
});
describe("from & serialize", () => {
it("should serialize to a placeholder representation", () => {
const answer = PollAnswerSubevent.from("one", "ONE");
expect(answer.id).toBe("one");
expect(answer.text).toBe("ONE");
const serialized = answer.serialize();
expect(serialized.type).toBe("org.matrix.sdk.poll.answer");
expect(serialized.content).toMatchObject({
id: "one",
[M_TEXT.name]: expect.any(String), // tested by MessageEvent
});
});
});
});
describe("PollStartEvent", () => {
// Note: throughout these tests we don't really bother testing that
// MessageEvent is doing its job. It has its own tests to worry about.
it("should parse a poll", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
{ id: "one", [M_TEXT.name]: "ONE" },
{ id: "two", [M_TEXT.name]: "TWO" },
{ id: "thr", [M_TEXT.name]: "THR" },
],
},
},
};
const poll = new PollStartEvent(input);
expect(poll.question).toBeDefined();
expect(poll.question.text).toBe("Question here");
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
expect(poll.maxSelections).toBe(2);
expect(poll.answers.length).toBe(3);
expect(poll.answers.some((a) => a.id === "one" && a.text === "ONE")).toBe(true);
expect(poll.answers.some((a) => a.id === "two" && a.text === "TWO")).toBe(true);
expect(poll.answers.some((a) => a.id === "thr" && a.text === "THR")).toBe(true);
});
it("should fail to parse a missing question", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
{ id: "one", [M_TEXT.name]: "ONE" },
{ id: "two", [M_TEXT.name]: "TWO" },
{ id: "thr", [M_TEXT.name]: "THR" },
],
},
} as any, // force invalid type
};
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("A question is required"));
});
it("should fail to parse non-array answers", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: "one",
} as any, // force invalid type
},
};
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("Poll answers must be an array"));
});
it("should fail to parse invalid answers", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [{ id: "one" }, { [M_TEXT.name]: "TWO" }],
} as any, // force invalid type
},
};
expect(() => new PollStartEvent(input)).toThrow(); // error tested by PollAnswerSubevent tests
});
it("should fail to parse lack of answers", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [],
} as any, // force invalid type
},
};
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("No answers available"));
});
it("should truncate answers at 20", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
{ id: "01", [M_TEXT.name]: "A" },
{ id: "02", [M_TEXT.name]: "B" },
{ id: "03", [M_TEXT.name]: "C" },
{ id: "04", [M_TEXT.name]: "D" },
{ id: "05", [M_TEXT.name]: "E" },
{ id: "06", [M_TEXT.name]: "F" },
{ id: "07", [M_TEXT.name]: "G" },
{ id: "08", [M_TEXT.name]: "H" },
{ id: "09", [M_TEXT.name]: "I" },
{ id: "10", [M_TEXT.name]: "J" },
{ id: "11", [M_TEXT.name]: "K" },
{ id: "12", [M_TEXT.name]: "L" },
{ id: "13", [M_TEXT.name]: "M" },
{ id: "14", [M_TEXT.name]: "N" },
{ id: "15", [M_TEXT.name]: "O" },
{ id: "16", [M_TEXT.name]: "P" },
{ id: "17", [M_TEXT.name]: "Q" },
{ id: "18", [M_TEXT.name]: "R" },
{ id: "19", [M_TEXT.name]: "S" },
{ id: "20", [M_TEXT.name]: "T" },
{ id: "FAIL", [M_TEXT.name]: "U" },
],
},
},
};
const poll = new PollStartEvent(input);
expect(poll.answers.length).toBe(20);
expect(poll.answers.some((a) => a.id === "FAIL")).toBe(false);
});
it("should infer a kind from unknown kinds", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
kind: "org.example.custom.poll.kind",
max_selections: 2,
answers: [
{ id: "01", [M_TEXT.name]: "A" },
{ id: "02", [M_TEXT.name]: "B" },
{ id: "03", [M_TEXT.name]: "C" },
],
},
},
};
const poll = new PollStartEvent(input);
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
expect(poll.rawKind).toBe("org.example.custom.poll.kind");
});
it("should infer a kind from missing kinds", () => {
const input: IPartialEvent<PollStartEventContent> = {
type: M_POLL_START.name,
content: {
[M_TEXT.name]: "FALLBACK Question here",
[M_POLL_START.name]: {
question: { [M_TEXT.name]: "Question here" },
max_selections: 2,
answers: [
{ id: "01", [M_TEXT.name]: "A" },
{ id: "02", [M_TEXT.name]: "B" },
{ id: "03", [M_TEXT.name]: "C" },
],
} as any, // force invalid type
},
};
const poll = new PollStartEvent(input);
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
expect(poll.rawKind).toBeFalsy();
});
describe("from & serialize", () => {
it("should serialize to a poll start event", () => {
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], M_POLL_KIND_DISCLOSED, 2);
expect(poll.question.text).toBe("Question here");
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
expect(poll.maxSelections).toBe(2);
expect(poll.answers.length).toBe(3);
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
// Ids are non-empty and unique
expect(poll.answers[0].id).toHaveLength(16);
expect(poll.answers[1].id).toHaveLength(16);
expect(poll.answers[2].id).toHaveLength(16);
expect(poll.answers[0].id).not.toEqual(poll.answers[1].id);
expect(poll.answers[0].id).not.toEqual(poll.answers[2].id);
expect(poll.answers[1].id).not.toEqual(poll.answers[2].id);
const serialized = poll.serialize();
expect(M_POLL_START.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
[M_POLL_START.name]: {
question: {
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
},
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 2,
answers: [
// M_TEXT tested by MessageEvent tests
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
],
},
});
});
it("should serialize to a custom kind poll start event", () => {
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], "org.example.poll.kind", 2);
expect(poll.question.text).toBe("Question here");
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
expect(poll.rawKind).toBe("org.example.poll.kind");
expect(poll.maxSelections).toBe(2);
expect(poll.answers.length).toBe(3);
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
const serialized = poll.serialize();
expect(M_POLL_START.matches(serialized.type)).toBe(true);
expect(serialized.content).toMatchObject({
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
[M_POLL_START.name]: {
question: {
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
},
kind: "org.example.poll.kind",
max_selections: 2,
answers: [
// M_MESSAGE tested by MessageEvent tests
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
],
},
});
});
});
});
@@ -0,0 +1,87 @@
/*
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { NamespacedValue } from "matrix-events-sdk";
import { isEventTypeSame } from "../../../src/@types/extensible_events";
describe("isEventTypeSame", () => {
it("should match string and string", () => {
const a = "org.example.message-like";
const b = "org.example.different";
expect(isEventTypeSame(a, b)).toBe(false);
expect(isEventTypeSame(b, a)).toBe(false);
expect(isEventTypeSame(a, a)).toBe(true);
expect(isEventTypeSame(b, b)).toBe(true);
});
it("should match string and namespace", () => {
const a = "org.example.message-like";
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
expect(isEventTypeSame(a, b)).toBe(false);
expect(isEventTypeSame(b, a)).toBe(false);
expect(isEventTypeSame(a, a)).toBe(true);
expect(isEventTypeSame(b, b)).toBe(true);
expect(isEventTypeSame(b.name, b)).toBe(true);
expect(isEventTypeSame(b.altName, b)).toBe(true);
expect(isEventTypeSame(b, b.name)).toBe(true);
expect(isEventTypeSame(b, b.altName)).toBe(true);
});
it("should match namespace and namespace", () => {
const a = new NamespacedValue<string, string>("org.example.stable1", "org.example.unstable1");
const b = new NamespacedValue<string, string>("org.example.stable2", "org.example.unstable2");
expect(isEventTypeSame(a, b)).toBe(false);
expect(isEventTypeSame(b, a)).toBe(false);
expect(isEventTypeSame(a, a)).toBe(true);
expect(isEventTypeSame(a.name, a)).toBe(true);
expect(isEventTypeSame(a.altName, a)).toBe(true);
expect(isEventTypeSame(a, a.name)).toBe(true);
expect(isEventTypeSame(a, a.altName)).toBe(true);
expect(isEventTypeSame(b, b)).toBe(true);
expect(isEventTypeSame(b.name, b)).toBe(true);
expect(isEventTypeSame(b.altName, b)).toBe(true);
expect(isEventTypeSame(b, b.name)).toBe(true);
expect(isEventTypeSame(b, b.altName)).toBe(true);
});
it("should match namespaces of different pointers", () => {
const a = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
expect(isEventTypeSame(a, b)).toBe(true);
expect(isEventTypeSame(b, a)).toBe(true);
expect(isEventTypeSame(a, a)).toBe(true);
expect(isEventTypeSame(a.name, a)).toBe(true);
expect(isEventTypeSame(a.altName, a)).toBe(true);
expect(isEventTypeSame(a, a.name)).toBe(true);
expect(isEventTypeSame(a, a.altName)).toBe(true);
expect(isEventTypeSame(b, b)).toBe(true);
expect(isEventTypeSame(b.name, b)).toBe(true);
expect(isEventTypeSame(b.altName, b)).toBe(true);
expect(isEventTypeSame(b, b.name)).toBe(true);
expect(isEventTypeSame(b, b.altName)).toBe(true);
});
});
+56 -50
View File
@@ -1,15 +1,15 @@
import { RelationType } from "../../src";
import { FilterComponent } from "../../src/filter-component";
import { mkEvent } from '../test-utils/test-utils';
import { mkEvent } from "../test-utils/test-utils";
describe("Filter Component", function() {
describe("types", function() {
it("should filter out events with other types", function() {
const filter = new FilterComponent({ types: ['m.room.message'] });
describe("Filter Component", function () {
describe("types", function () {
it("should filter out events with other types", function () {
const filter = new FilterComponent({ types: ["m.room.message"] });
const event = mkEvent({
type: 'm.room.member',
content: { },
room: 'roomId',
type: "m.room.member",
content: {},
room: "roomId",
event: true,
});
@@ -18,12 +18,12 @@ describe("Filter Component", function() {
expect(checkResult).toBe(false);
});
it("should validate events with the same type", function() {
const filter = new FilterComponent({ types: ['m.room.message'] });
it("should validate events with the same type", function () {
const filter = new FilterComponent({ types: ["m.room.message"] });
const event = mkEvent({
type: 'm.room.message',
content: { },
room: 'roomId',
type: "m.room.message",
content: {},
room: "roomId",
event: true,
});
@@ -32,17 +32,20 @@ describe("Filter Component", function() {
expect(checkResult).toBe(true);
});
it("should filter out events by relation participation", function() {
const currentUserId = '@me:server.org';
const filter = new FilterComponent({
related_by_senders: [currentUserId],
}, currentUserId);
it("should filter out events by relation participation", function () {
const currentUserId = "@me:server.org";
const filter = new FilterComponent(
{
related_by_senders: [currentUserId],
},
currentUserId,
);
const threadRootNotParticipated = mkEvent({
type: 'm.room.message',
type: "m.room.message",
content: {},
room: 'roomId',
user: '@someone-else:server.org',
room: "roomId",
user: "@someone-else:server.org",
event: true,
unsigned: {
"m.relations": {
@@ -57,14 +60,17 @@ describe("Filter Component", function() {
expect(filter.check(threadRootNotParticipated)).toBe(false);
});
it("should keep events by relation participation", function() {
const currentUserId = '@me:server.org';
const filter = new FilterComponent({
related_by_senders: [currentUserId],
}, currentUserId);
it("should keep events by relation participation", function () {
const currentUserId = "@me:server.org";
const filter = new FilterComponent(
{
related_by_senders: [currentUserId],
},
currentUserId,
);
const threadRootParticipated = mkEvent({
type: 'm.room.message',
type: "m.room.message",
content: {},
unsigned: {
"m.relations": {
@@ -74,23 +80,23 @@ describe("Filter Component", function() {
},
},
},
user: '@someone-else:server.org',
room: 'roomId',
user: "@someone-else:server.org",
room: "roomId",
event: true,
});
expect(filter.check(threadRootParticipated)).toBe(true);
});
it("should filter out events by relation type", function() {
it("should filter out events by relation type", function () {
const filter = new FilterComponent({
related_by_rel_types: ["m.thread"],
});
const referenceRelationEvent = mkEvent({
type: 'm.room.message',
type: "m.room.message",
content: {},
room: 'roomId',
room: "roomId",
event: true,
unsigned: {
"m.relations": {
@@ -102,13 +108,13 @@ describe("Filter Component", function() {
expect(filter.check(referenceRelationEvent)).toBe(false);
});
it("should keep events by relation type", function() {
it("should keep events by relation type", function () {
const filter = new FilterComponent({
related_by_rel_types: ["m.thread"],
});
const threadRootEvent = mkEvent({
type: 'm.room.message',
type: "m.room.message",
content: {},
unsigned: {
"m.relations": {
@@ -118,22 +124,22 @@ describe("Filter Component", function() {
},
},
},
room: 'roomId',
room: "roomId",
event: true,
});
const eventWithMultipleRelations = mkEvent({
"type": "m.room.message",
"content": {},
"unsigned": {
type: "m.room.message",
content: {},
unsigned: {
"m.relations": {
"testtesttest": {},
"m.annotation": {
"chunk": [
chunk: [
{
"type": "m.reaction",
"key": "🤫",
"count": 1,
type: "m.reaction",
key: "🤫",
count: 1,
},
],
},
@@ -143,20 +149,20 @@ describe("Filter Component", function() {
},
},
},
"room": 'roomId',
"event": true,
room: "roomId",
event: true,
});
const noMatchEvent = mkEvent({
"type": "m.room.message",
"content": {},
"unsigned": {
type: "m.room.message",
content: {},
unsigned: {
"m.relations": {
"testtesttest": {},
testtesttest: {},
},
},
"room": 'roomId',
"event": true,
room: "roomId",
event: true,
});
expect(filter.check(threadRootEvent)).toBe(true);
+11 -11
View File
@@ -19,17 +19,17 @@ import { Filter, IFilterDefinition } from "../../src/filter";
import { mkEvent } from "../test-utils/test-utils";
import { EventType } from "../../src";
describe("Filter", function() {
describe("Filter", function () {
const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger";
let filter: Filter;
beforeEach(function() {
beforeEach(function () {
filter = new Filter(userId);
});
describe("fromJson", function() {
it("create a new Filter from the provided values", function() {
describe("fromJson", function () {
it("create a new Filter from the provided values", function () {
const definition = {
event_fields: ["type", "content"],
};
@@ -40,8 +40,8 @@ describe("Filter", function() {
});
});
describe("setTimelineLimit", function() {
it("should set room.timeline.limit of the filter definition", function() {
describe("setTimelineLimit", function () {
it("should set room.timeline.limit of the filter definition", function () {
filter.setTimelineLimit(10);
expect(filter.getDefinition()).toEqual({
room: {
@@ -53,18 +53,18 @@ describe("Filter", function() {
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
describe("setDefinition/getDefinition", function () {
it("should set and get the filter body", function () {
const definition = {
event_format: "client" as IFilterDefinition['event_format'],
event_format: "client" as IFilterDefinition["event_format"],
};
filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition);
});
});
describe("setUnreadThreadNotifications", function() {
it("setUnreadThreadNotifications", function() {
describe("setUnreadThreadNotifications", function () {
it("setUnreadThreadNotifications", function () {
filter.setUnreadThreadNotifications(true);
expect(filter.getDefinition()).toEqual({
room: {

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