Compare commits

...

103 Commits

Author SHA1 Message Date
RiotRobot 81c3668cb6 v21.0.1 2022-11-01 09:16:11 +00:00
RiotRobot 8f40dc6304 Prepare changelog for v21.0.1 2022-11-01 09:16:11 +00:00
Michael Telatynski fcdd8c93f4 [Backport staging] Catch server versions API call exception when starting the client (#2832)
Co-authored-by: Germain <germains@element.io>
2022-11-01 09:01:52 +00:00
ElementRobot 7d7803380c [Backport staging] Fix default behavior of Room.getBlacklistUnverifiedDevices (#2831)
Co-authored-by: Faye Duxovni <fayed@matrix.org>
2022-11-01 08:47:47 +00:00
ElementRobot 545a74364d [Backport staging] Fix authedRequest including Authorization: Bearer undefined for password resets (#2829)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-31 17:14:06 +00:00
ElementRobot db33f396b8 [Backport staging] Fix JSDoc (#2827)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-31 17:00:45 +00:00
RiotRobot 1842004db2 v21.0.0 2022-10-25 17:04:02 +01:00
RiotRobot c8c7af0ae2 Prepare changelog for v21.0.0 2022-10-25 17:04:02 +01:00
RiotRobot e7ce1fb9e8 v21.0.0-rc.2 2022-10-24 16:19:47 +01:00
RiotRobot 7772f855e6 Prepare changelog for v21.0.0-rc.2 2022-10-24 16:19:46 +01:00
ElementRobot 4ccc52da8e [Backport staging] Improve crypto init code and allow easier shimming (#2792)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-24 10:12:42 +01:00
ElementRobot 63f4bf571e [Backport staging] Fix POST data not being passed for registerWithIdentityServer (#2770)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-10-18 15:03:40 +00:00
RiotRobot fc1b03c0bf v21.0.0-rc.1 2022-10-18 12:49:22 +01:00
RiotRobot e592f60240 Prepare changelog for v21.0.0-rc.1 2022-10-18 12:49:21 +01:00
Michael Telatynski 30570bcce6 Remove node-specific crypto bits, use Node 16's WebCrypto (#2762) 2022-10-17 17:54:54 +01:00
Michael Telatynski 6af3b114e1 Fix IdentityPrefix.V2 containing spurious /api (#2761) 2022-10-17 14:32:40 +01:00
Michael Telatynski 041f9951c5 Remove deprecated m.room.aliases references (#2759) 2022-10-17 10:58:48 +01:00
Germain be11fa6b5a Fix notification type when resetting threads notifications (#2757) 2022-10-17 09:27:28 +01:00
Stanislav Demydiuk 6245661cd7 Export types for MatrixEvent and Room emitted events, and make event handler map types stricter (#2750) 2022-10-14 21:18:00 -04:00
Michael Telatynski 12a4d2a749 Make more of the code conform to Strict TSC (#2756) 2022-10-14 15:57:08 +01:00
kegsay f70f6db926 Merge pull request #2753 from matrix-org/kegan/http-code-on-non-json
Always send back an httpStatus property if one is known
2022-10-14 10:31:12 +01:00
Kegan Dougal 500601ea85 More tests to satisfy sonarcloud 2022-10-14 10:23:18 +01:00
Kegan Dougal e32dfccbd9 Additional tests for 'url' property to satisfy code coverage 2022-10-13 15:39:23 +01:00
Kegan Dougal ed78737768 Fix types on utils UTs 2022-10-13 15:19:26 +01:00
Kegan Dougal ac561b743b Linting 2022-10-13 15:04:01 +01:00
Kegan Dougal e8be7af751 err already sets httpStatus now 2022-10-13 15:02:52 +01:00
Kegan Dougal 400b457edf Fix broken tests 2022-10-13 14:59:15 +01:00
Kegan Dougal 5ed4e9f535 Always send back an httpStatus property if one is known
Previously, non-JSON responses would be missing the `httpStatus`
property, which was different to how `request()` used to work.

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

* fix ts issue in devicelist-integ.spec

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

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

* strict fixes

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

* fix ts issues

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

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

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

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

* remove obsoleted prev_events from mockenvents

* make xmlhttprequest ts

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

* strict in devicelist

* strict fixes in matrix-client-crypto.spec

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

* strict issues in matrix-client-opts.specc

* strict issues in matrix-client-syncing

* strict issues in spec/integ/megolm

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

* strict fixes for spec/integ/sliding-sync

* eslint fixes

* more strict errors sneaking in from develop

* kill al httpbackends

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

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

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

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

* add ubstable value

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

* lint fix
2022-09-28 15:39:37 +01:00
Germain 0403e4bedc Fix incorrect MSC3890 unstable prefix (#2703) 2022-09-28 15:37:13 +02:00
RiotRobot 14aa7846a5 Merge branch 'master' into develop 2022-09-28 14:05:21 +01:00
RiotRobot 45348a354e Resetting package fields for development 2022-09-27 16:48:03 +01:00
RiotRobot fa3339fc84 Merge branch 'master' into develop 2022-09-27 16:48:00 +01:00
Germain b332c6c4b9 Account data can return undefined (#2701) 2022-09-27 12:47:05 +00:00
Germain 209a101be7 Add local notification settings capability (#2700) 2022-09-27 11:41:20 +01:00
Michael Telatynski efbf5479d1 Remove redundant rel_branch logic (#2698) 2022-09-26 10:05:58 +01:00
Hugh Nimmo-Smith dacef048be Typings for MSC3824 (#2691) 2022-09-23 16:23:50 -06:00
Hugh Nimmo-Smith caadc6f95b Implementation of MSC3882 login token request (#2687) 2022-09-22 16:36:37 +01:00
Hugh Nimmo-Smith 39bc7e2bb3 Refer to explicit Matrix spec versions instead of Latest (#2668) 2022-09-22 15:28:11 +00:00
Hugh Nimmo-Smith 040f012350 Typings for MSC2965 OIDC provider discovery (#2424) 2022-09-22 16:27:13 +01:00
Hugh Nimmo-Smith 8b2b677f92 Add return type for loginFlows() (#2669)
Co-authored-by: Travis Ralston <travisr@matrix.org>
2022-09-22 16:25:01 +01:00
Germain 2b1fab928b Add unstable device_id field for MSC3881 (#2688) 2022-09-22 15:44:54 +02:00
Germain 516f52c5a4 Support to remotely toggle push notifications (#2686) 2022-09-22 08:37:54 +00:00
Michael Telatynski 8599a98b47 Fix backpagination at end logic being spec non-conforming (#2680) 2022-09-21 16:35:07 +01:00
renovate[bot] 7e24cb6cae Update jest monorepo to v29.0.3 (#2682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 09:25:43 +01:00
renovate[bot] 38aa8d18c0 Update all (#2684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 09:13:36 +01:00
Germain 2967ee6309 Read receipts for threads (#2635) 2022-09-21 07:50:44 +00:00
renovate[bot] 2e10b6065c Update typescript-eslint monorepo to v5.38.0 (#2685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 08:42:30 +02:00
renovate[bot] 69057ee035 Update babel monorepo to v7.19.1 (#2681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 08:25:16 +02:00
Kegan Dougal 4059b5bfba Merge branch 'develop' into kegan/sync-v3 2022-08-23 15:57:08 +01:00
Kegan Dougal 3a120f8fb8 Assert pos values as well 2022-08-19 18:19:24 +01:00
Kegan Dougal d2535b8516 Linting 2022-08-19 18:15:33 +01:00
Kegan Dougal 2cda229bc4 Automatically reconnect sessions when sliding sync expires them
This can happen when you close your laptop overnight,
as the server will not hold onto in-memory resources
for your connection indefinitely. When this happen,
the server will HTTP 400 you with "session expired".

At this point, it is no longer safe to remember anything
and you must forget everything and resend any sticky
parameters. This commit does the sticky parameters and
re-establishes the connection, but it may need additional
work to make the JS SDK forget now invalid data.
2022-08-19 17:33:07 +01:00
130 changed files with 8548 additions and 5569 deletions
+21 -1
View File
@@ -1,14 +1,22 @@
module.exports = {
plugins: [
"matrix-org",
"import",
],
extends: [
"plugin:matrix-org/babel",
"plugin:import/typescript",
],
env: {
browser: true,
node: true,
},
settings: {
"import/resolver": {
typescript: true,
node: true,
},
},
// NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: {
@@ -35,7 +43,19 @@ module.exports = {
"no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", "events"],
"no-restricted-imports": ["error", {
name: "events",
message: "Please use TypedEventEmitter instead"
}],
"import/no-restricted-paths": ["error", {
"zones": [{
"target": "./src/",
"from": "./src/index.ts",
"message": "The package index is dynamic between src and lib depending on " +
"whether release or development, target the specific module or matrix.ts instead",
}],
}],
},
overrides: [{
files: [
+2 -1
View File
@@ -36,5 +36,6 @@ jobs:
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.version }}
+10
View File
@@ -23,6 +23,16 @@ jobs:
- name: Typecheck
run: "yarn run lint:types"
- name: Switch js-sdk to release mode
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Typecheck (release mode)
run: "yarn run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
+51
View File
@@ -1,3 +1,54 @@
Changes in [21.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.1) (2022-11-01)
==================================================================================================
## 🐛 Bug Fixes
* Fix default behavior of Room.getBlacklistUnverifiedDevices ([\#2830](https://github.com/matrix-org/matrix-js-sdk/pull/2830)). Contributed by @duxovni.
* Catch server versions API call exception when starting the client ([\#2828](https://github.com/matrix-org/matrix-js-sdk/pull/2828)). Fixes vector-im/element-web#23634.
* Fix authedRequest including `Authorization: Bearer undefined` for password resets ([\#2822](https://github.com/matrix-org/matrix-js-sdk/pull/2822)). Fixes vector-im/element-web#23655.
Changes in [21.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.0.0) (2022-10-25)
==================================================================================================
## 🚨 BREAKING CHANGES
* Changes the `uploadContent` API, kills off `request` and `browser-request` in favour of `fetch`, removed callback support on a lot of the methods, adds a lot of tests. ([\#2719](https://github.com/matrix-org/matrix-js-sdk/pull/2719)). Fixes #2415 and #801.
* Remove deprecated `m.room.aliases` references ([\#2759](https://github.com/matrix-org/matrix-js-sdk/pull/2759)). Fixes vector-im/element-web#12680.
## ✨ Features
* Remove node-specific crypto bits, use Node 16's WebCrypto ([\#2762](https://github.com/matrix-org/matrix-js-sdk/pull/2762)). Fixes #2760.
* Export types for MatrixEvent and Room emitted events, and make event handler map types stricter ([\#2750](https://github.com/matrix-org/matrix-js-sdk/pull/2750)). Contributed by @stas-demydiuk.
* Use even more stable calls to `/room_keys` ([\#2746](https://github.com/matrix-org/matrix-js-sdk/pull/2746)).
* Upgrade to Olm 3.2.13 which has been repackaged to support Node 18 ([\#2744](https://github.com/matrix-org/matrix-js-sdk/pull/2744)).
* Fix `power_level_content_override` type ([\#2741](https://github.com/matrix-org/matrix-js-sdk/pull/2741)).
* Add custom notification handling for MSC3401 call events ([\#2720](https://github.com/matrix-org/matrix-js-sdk/pull/2720)).
* Add support for unread thread notifications ([\#2726](https://github.com/matrix-org/matrix-js-sdk/pull/2726)).
* Load Thread List with server-side assistance (MSC3856) ([\#2602](https://github.com/matrix-org/matrix-js-sdk/pull/2602)).
* Use stable calls to `/room_keys` ([\#2729](https://github.com/matrix-org/matrix-js-sdk/pull/2729)). Fixes vector-im/element-web#22839.
## 🐛 Bug Fixes
* Fix POST data not being passed for registerWithIdentityServer ([\#2769](https://github.com/matrix-org/matrix-js-sdk/pull/2769)). Fixes matrix-org/element-web-rageshakes#16206.
* Fix IdentityPrefix.V2 containing spurious `/api` ([\#2761](https://github.com/matrix-org/matrix-js-sdk/pull/2761)). Fixes vector-im/element-web#23505.
* Always send back an httpStatus property if one is known ([\#2753](https://github.com/matrix-org/matrix-js-sdk/pull/2753)).
* Check for AbortError, not any generic connection error, to avoid tightlooping ([\#2752](https://github.com/matrix-org/matrix-js-sdk/pull/2752)).
* Correct the dir parameter of MSC3715 ([\#2745](https://github.com/matrix-org/matrix-js-sdk/pull/2745)). Contributed by @dhenneke.
* Fix sync init when thread unread notif is not supported ([\#2739](https://github.com/matrix-org/matrix-js-sdk/pull/2739)). Fixes vector-im/element-web#23435.
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
Changes in [20.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0) (2022-10-11)
============================================================================================================
## ✨ Features
* Add local notification settings capability ([\#2700](https://github.com/matrix-org/matrix-js-sdk/pull/2700)).
* Implementation of MSC3882 login token request ([\#2687](https://github.com/matrix-org/matrix-js-sdk/pull/2687)). Contributed by @hughns.
* Typings for MSC2965 OIDC provider discovery ([\#2424](https://github.com/matrix-org/matrix-js-sdk/pull/2424)). Contributed by @hughns.
* Support to remotely toggle push notifications ([\#2686](https://github.com/matrix-org/matrix-js-sdk/pull/2686)).
* Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)).
## 🐛 Bug Fixes
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
* Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee.
* Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377.
* Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784.
Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30)
==================================================================================================
+2 -4
View File
@@ -33,10 +33,8 @@ In Node.js
----------
Ensure you have the latest LTS version of Node.js installed.
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
a bundler like webpack you'll likely have to transpile dependencies, including this
SDK, to match your target browsers.
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
+13 -9
View File
@@ -1,9 +1,9 @@
{
"name": "matrix-js-sdk",
"version": "20.0.2",
"version": "21.0.1",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=12.9.0"
"node": ">=16.0.0"
},
"scripts": {
"prepublishOnly": "yarn build",
@@ -55,14 +55,12 @@
"dependencies": {
"@babel/runtime": "^7.12.5",
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.7",
"p-retry": "4",
"qs": "^6.9.6",
"request": "^2.88.2",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
@@ -78,12 +76,12 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
"@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",
"@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
@@ -92,17 +90,20 @@
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "8.23.0",
"domexception": "^4.0.0",
"eslint": "8.24.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-matrix-org": "^0.6.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"jest": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^27.5.1",
"jest-sonar-reporter": "^2.0.0",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^2.1.2",
"matrix-mock-request": "^2.5.0",
"rimraf": "^3.0.2",
"terser": "^5.5.1",
"tsify": "^5.0.2",
@@ -113,6 +114,9 @@
"testMatch": [
"<rootDir>/spec/**/*.spec.{js,ts}"
],
"setupFilesAfterEnv": [
"<rootDir>/spec/setupTests.ts"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts}"
],
+1 -13
View File
@@ -135,7 +135,6 @@ yarn install --ignore-scripts --pure-lockfile
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
prerelease=0
# We check if this build is a prerelease by looking to
@@ -150,18 +149,7 @@ else
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
fi
# We might already be on the release branch, in which case, yay
# If we're on any branch starting with 'release', or the staging branch
# we don't create a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* && "$curbranch" != "staging" ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
rel_branch=$(git symbolic-ref --short HEAD)
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env node
const fsProm = require('fs/promises');
const PKGJSON = 'package.json';
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
for (const field of ['main', 'typings']) {
if (pkgJson["matrix_lib_"+field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_"+field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
+2 -3
View File
@@ -30,7 +30,6 @@ import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils";
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
import { IKeyBackupSession } from "../src/crypto/keybackup";
import { IHttpOpts } from "../src/http-api";
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
/**
@@ -56,11 +55,11 @@ export class TestClient {
this.httpBackend = new MockHttpBackend();
const fullOptions: ICreateClientOpts = {
baseUrl: "http://" + userId + ".test.server",
baseUrl: "http://" + userId?.slice(1).replace(":", ".") + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
request: this.httpBackend.requestFn as IHttpOpts["request"],
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
...options,
};
if (!fullOptions.cryptoStore) {
@@ -15,9 +15,11 @@ limitations under the License.
*/
// stub for browser-matrix browserify tests
// @ts-ignore
global.XMLHttpRequest = jest.fn();
afterAll(() => {
// clean up XMLHttpRequest mock
// @ts-ignore
global.XMLHttpRequest = undefined;
});
+50 -25
View File
@@ -14,46 +14,66 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// load XmlHttpRequest mock
import HttpBackend from "matrix-mock-request";
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import type { MatrixClient, ClientEvent } from "../../src";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
matrixcs: {
MatrixClient: typeof MatrixClient;
ClientEvent: typeof ClientEvent;
};
}
}
}
describe("Browserify Test", function() {
let client;
let httpBackend;
let client: MatrixClient;
let httpBackend: HttpBackend;
beforeEach(() => {
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend = new HttpBackend();
client = new global.matrixcs.MatrixClient({
baseUrl: "http://test.server",
userId: USER_ID,
accessToken: ACCESS_TOKEN,
deviceId: DEVICE_ID,
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
client.startClient();
});
afterEach(async () => {
client.stopClient();
httpBackend.stop();
client.http.abort();
httpBackend.verifyNoOutstandingRequests();
httpBackend.verifyNoOutstandingExpectation();
await httpBackend.stop();
});
it("Sync", function() {
const event = utils.mkMembership({
room: ROOM_ID,
mship: "join",
user: "@other_user:server.test",
name: "Displayname",
});
it("Sync", async () => {
const event = {
type: "m.room.member",
room_id: ROOM_ID,
content: {
membership: "join",
name: "Displayname",
},
event_id: "$foobar",
};
const syncData = {
next_batch: "batch1",
@@ -71,11 +91,16 @@ describe("Browserify Test", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
return Promise.race([
httpBackend.flushAllExpected(),
new Promise((_, reject) => {
client.once("sync.unexpectedError", reject);
}),
]);
httpBackend.when("GET", "/sync").respond(200, syncData);
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
const unexpectedErrorFn = jest.fn();
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
client.startClient();
await httpBackend.flushAllExpected();
await syncPromise;
expect(unexpectedErrorFn).not.toHaveBeenCalled();
}, 20000); // additional timeout as this test can take quite a while
});
@@ -122,7 +122,7 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
event_id: '$event_id',
});
return Promise.all([
@@ -290,8 +290,9 @@ describe("DeviceList management:", function() {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list",
0,
);
});
});
@@ -326,8 +327,9 @@ describe("DeviceList management:", function() {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
0,
);
});
});
@@ -362,8 +364,9 @@ describe("DeviceList management:", function() {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
0,
);
});
});
@@ -378,13 +381,15 @@ describe("DeviceList management:", function() {
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
await anotherTestClient.flushSync();
await anotherTestClient.client.crypto.deviceList.saveIfDirty();
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
// @ts-ignore accessing private property
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
0,
);
});
} finally {
+17 -17
View File
@@ -31,8 +31,9 @@ import '../olm-loader';
import { logger } from '../../src/logger';
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED } from "../../src/client";
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from '../../src/crypto/deviceinfo';
let aliTestClient: TestClient;
const roomId = "!room:localhost";
@@ -71,12 +72,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<num
expect(uploader.deviceKeys).toBeTruthy();
const uploaderKeys = {};
uploaderKeys[uploader.deviceId] = uploader.deviceKeys;
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
querier.httpBackend.when("POST", "/keys/query")
.respond(200, function(_path, content) {
expect(content.device_keys[uploader.userId]).toEqual([]);
.respond(200, function(_path, content: IUploadKeysRequest) {
expect(content.device_keys![uploader.userId!]).toEqual([]);
const result = {};
result[uploader.userId] = uploaderKeys;
result[uploader.userId!] = uploaderKeys;
return { device_keys: result };
});
return querier.httpBackend.flush("/keys/query", 1);
@@ -93,10 +94,10 @@ async function expectAliClaimKeys(): Promise<void> {
const keys = await bobTestClient.awaitOneTimeKeyUpload();
aliTestClient.httpBackend.when(
"POST", "/keys/claim",
).respond(200, function(_path, content) {
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
).respond(200, function(_path, content: IUploadKeysRequest) {
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = null;
let keyId = '';
for (keyId in keys) {
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
@@ -135,10 +136,10 @@ async function aliDownloadsKeys(): Promise<void> {
await aliTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore - protected
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data.devices[bobUserId];
const devices = data!.devices[bobUserId]!;
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
});
}
@@ -237,7 +238,7 @@ function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
const path = "/send/m.room.encrypted/";
const prom = new Promise((resolve) => {
const prom = new Promise<IContent>((resolve) => {
httpBackend.when("PUT", path).respond(200, function(_path, content) {
resolve(content);
return {
@@ -252,14 +253,14 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
}
function aliRecvMessage(): Promise<void> {
const message = bobMessages.shift();
const message = bobMessages.shift()!;
return recvMessage(
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
);
}
function bobRecvMessage(): Promise<void> {
const message = aliMessages.shift();
const message = aliMessages.shift()!;
return recvMessage(
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
);
@@ -509,7 +510,7 @@ describe("MatrixClient crypto", () => {
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
const message = aliMessages.shift();
const message = aliMessages.shift()!;
const syncData = {
next_batch: "x",
rooms: {
@@ -664,11 +665,10 @@ describe("MatrixClient crypto", () => {
]);
logger.log(aliTestClient + ': started');
httpBackend.when("POST", "/keys/upload")
.respond(200, (_path, content) => {
.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);
logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length);
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
// cancel futher calls by telling the client
// we have more than we need
return {
@@ -1,25 +1,59 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import {
ClientEvent,
HttpApiEvent,
IEvent,
MatrixClient,
RoomEvent,
RoomMemberEvent,
RoomStateEvent,
UserEvent,
} from "../../src";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient events", function() {
let client;
let httpBackend;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const setupTests = (): [MatrixClient, HttpBackend] => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
const client = testClient.client;
const httpBackend = testClient.httpBackend;
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
return [client!, httpBackend];
};
beforeEach(function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
[client!, httpBackend] = setupTests();
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
httpBackend?.verifyNoOutstandingExpectation();
client?.stopClient();
return httpBackend?.stop();
});
describe("emissions", function() {
@@ -92,53 +126,49 @@ describe("MatrixClient events", function() {
};
it("should emit events from both the first and subsequent /sync calls",
function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
function() {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let expectedEvents = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
let expectedEvents: Partial<IEvent>[] = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
client.on("event", function(event) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
client!.on(ClientEvent.Event, function(event) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
}
}
}
expect(found).toBe(
true, "Unexpected 'event' emitted: " + event.getType(),
);
});
expect(found).toBe(true);
});
client.startClient();
client!.startClient();
return Promise.all([
return Promise.all([
// wait for two SYNCING events
utils.syncPromise(client).then(() => {
return utils.syncPromise(client);
}),
httpBackend.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(
0, "Failed to see all events from /sync calls",
);
utils.syncPromise(client!).then(() => {
return utils.syncPromise(client!);
}),
httpBackend!.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(0);
});
});
});
it("should emit User events", function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let fired = false;
client.on("User.presence", function(event, user) {
client!.on(UserEvent.Presence, function(event, user) {
fired = true;
expect(user).toBeTruthy();
expect(event).toBeTruthy();
@@ -146,58 +176,52 @@ describe("MatrixClient events", function() {
return;
}
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
SYNC_DATA.presence.events[0].content.presence,
SYNC_DATA.presence.events[0]?.content?.presence,
);
});
client.startClient();
client!.startClient();
httpBackend.flushAllExpected().then(function() {
expect(fired).toBe(true, "User.presence didn't fire.");
httpBackend!.flushAllExpected().then(function() {
expect(fired).toBe(true);
done();
});
});
it("should emit Room events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let roomInvokeCount = 0;
let roomNameInvokeCount = 0;
let timelineFireCount = 0;
client.on("Room", function(room) {
client!.on(ClientEvent.Room, function(room) {
roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar");
});
client.on("Room.timeline", function(event, room) {
client!.on(RoomEvent.Timeline, function(event, room) {
timelineFireCount++;
expect(room.roomId).toEqual("!erufh:bar");
expect(room?.roomId).toEqual("!erufh:bar");
});
client.on("Room.name", function(room) {
client!.on(RoomEvent.Name, function(room) {
roomNameInvokeCount++;
});
client.startClient();
client!.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(roomInvokeCount).toEqual(
1, "Room fired wrong number of times.",
);
expect(roomNameInvokeCount).toEqual(
1, "Room.name fired wrong number of times.",
);
expect(timelineFireCount).toEqual(
3, "Room.timeline fired the wrong number of times",
);
expect(roomInvokeCount).toEqual(1);
expect(roomNameInvokeCount).toEqual(1);
expect(timelineFireCount).toEqual(3);
});
});
it("should emit RoomState events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
const roomStateEventTypes = [
"m.room.member", "m.room.create",
@@ -205,126 +229,106 @@ describe("MatrixClient events", function() {
let eventsInvokeCount = 0;
let membersInvokeCount = 0;
let newMemberInvokeCount = 0;
client.on("RoomState.events", function(event, state) {
client!.on(RoomStateEvent.Events, function(event, state) {
eventsInvokeCount++;
const index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
-1, "Unexpected room state event type: " + event.getType(),
);
expect(index).not.toEqual(-1);
if (index >= 0) {
roomStateEventTypes.splice(index, 1);
}
});
client.on("RoomState.members", function(event, state, member) {
client!.on(RoomStateEvent.Members, function(event, state, member) {
membersInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toEqual("join");
});
client.on("RoomState.newMember", function(event, state, member) {
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
newMemberInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toBeFalsy();
});
client.startClient();
client!.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(membersInvokeCount).toEqual(
1, "RoomState.members fired wrong number of times",
);
expect(newMemberInvokeCount).toEqual(
1, "RoomState.newMember fired wrong number of times",
);
expect(eventsInvokeCount).toEqual(
2, "RoomState.events fired wrong number of times",
);
expect(membersInvokeCount).toEqual(1);
expect(newMemberInvokeCount).toEqual(1);
expect(eventsInvokeCount).toEqual(2);
});
});
it("should emit RoomMember events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let typingInvokeCount = 0;
let powerLevelInvokeCount = 0;
let nameInvokeCount = 0;
let membershipInvokeCount = 0;
client.on("RoomMember.name", function(event, member) {
client!.on(RoomMemberEvent.Name, function(event, member) {
nameInvokeCount++;
});
client.on("RoomMember.typing", function(event, member) {
client!.on(RoomMemberEvent.Typing, function(event, member) {
typingInvokeCount++;
expect(member.typing).toBe(true);
});
client.on("RoomMember.powerLevel", function(event, member) {
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
powerLevelInvokeCount++;
});
client.on("RoomMember.membership", function(event, member) {
client!.on(RoomMemberEvent.Membership, function(event, member) {
membershipInvokeCount++;
expect(member.membership).toEqual("join");
});
client.startClient();
client!.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
expect(typingInvokeCount).toEqual(
1, "RoomMember.typing fired wrong number of times",
);
expect(powerLevelInvokeCount).toEqual(
0, "RoomMember.powerLevel fired wrong number of times",
);
expect(nameInvokeCount).toEqual(
0, "RoomMember.name fired wrong number of times",
);
expect(membershipInvokeCount).toEqual(
1, "RoomMember.membership fired wrong number of times",
);
expect(typingInvokeCount).toEqual(1);
expect(powerLevelInvokeCount).toEqual(0);
expect(nameInvokeCount).toEqual(0);
expect(membershipInvokeCount).toEqual(1);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN' };
httpBackend.when("GET", "/sync").respond(401, error);
httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
client!.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
return httpBackend!.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(1);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
httpBackend.when("GET", "/sync").respond(401, error);
httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
client!.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
return httpBackend!.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(1);
});
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5,10 +5,11 @@ import { MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api";
import { IStore } from "../../src/store";
describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something";
let httpBackend = null;
let httpBackend = new HttpBackend();
const userId = "@alice:localhost";
const userB = "@bob:localhost";
const accessToken = "aseukfgwef";
@@ -67,7 +68,7 @@ describe("MatrixClient opts", function() {
let client;
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: undefined,
baseUrl: baseUrl,
userId: userId,
@@ -99,7 +100,7 @@ describe("MatrixClient opts", function() {
];
client.on("event", function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1, "Recv unexpected event type: " + event.getType(),
-1,
);
expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1,
@@ -118,7 +119,7 @@ describe("MatrixClient opts", function() {
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
0,
);
});
});
@@ -127,8 +128,8 @@ describe("MatrixClient opts", function() {
let client;
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
store: new MemoryStore(),
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: new MemoryStore() as IStore,
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -141,12 +142,12 @@ describe("MatrixClient opts", function() {
});
it("shouldn't retry sending events", function(done) {
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({
errcode: "M_SOMETHING",
error: "Ruh roh",
}));
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
expect(false).toBe(true);
}, function(err) {
expect(err.errcode).toEqual("M_SOMETHING");
done();
+127
View File
@@ -0,0 +1,127 @@
/*
Copyright 2022 Dominik Henneke
Copyright 2022 Nordeck IT + Consulting GmbH.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import { Direction, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { TestClient } from "../TestClient";
describe("MatrixClient relations", () => {
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const setupTests = (): [MatrixClient, HttpBackend] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ scheduler },
);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
return [client, httpBackend];
};
beforeEach(() => {
[client, httpBackend] = setupTests();
});
afterEach(() => {
httpBackend!.verifyNoOutstandingExpectation();
return httpBackend!.stop();
});
it("should read related events with the default options", async () => {
const response = client!.relations(roomId, '$event-0', null, null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
});
it("should read related events with relation type", async () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
});
it("should read related events with relation type and event type", async () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
});
it("should read related events with custom options", async () => {
const response = client!.relations(roomId, '$event-0', null, null, {
dir: Direction.Forward,
from: 'FROM',
limit: 10,
to: 'TO',
});
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
});
it('should use default direction in the fetchRelations endpoint', async () => {
const response = client!.fetchRelations(roomId, '$event-0', null, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" });
});
});
+29 -23
View File
@@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventStatus, RoomEvent, MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import HttpBackend from "matrix-mock-request";
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() {
let client: MatrixClient = null;
let httpBackend: TestClient["httpBackend"] = null;
let scheduler;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let room: Room;
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
let room: Room | undefined;
beforeEach(function() {
scheduler = new MatrixScheduler();
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
@@ -37,15 +37,21 @@ describe("MatrixClient retrying", function() {
undefined,
{ scheduler },
);
httpBackend = testClient.httpBackend;
client = testClient.client;
room = new Room(roomId, client, userId);
client.store.storeRoom(room);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
const room = new Room(roomId, client, userId);
client!.store.storeRoom(room);
return [client, httpBackend, room];
};
beforeEach(function() {
[client, httpBackend, room] = setupTests();
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
httpBackend!.verifyNoOutstandingExpectation();
return httpBackend!.stop();
});
xit("should retry according to MatrixScheduler.retryFn", function() {
@@ -66,7 +72,7 @@ describe("MatrixClient retrying", function() {
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued
const p1 = client.sendMessage(roomId, {
const p1 = client!.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m1",
}).then(function() {
@@ -79,13 +85,13 @@ describe("MatrixClient retrying", function() {
// XXX: it turns out that the promise returned by this message
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client.sendMessage(roomId, {
client!.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m2",
});
// both events should be in the timeline at this point
const tl = room.getLiveTimeline().getEvents();
const tl = room!.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
const ev1 = tl[0];
const ev2 = tl[1];
@@ -94,24 +100,24 @@ describe("MatrixClient retrying", function() {
expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
client!.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() {
client.cancelPendingEvent(ev1);
client!.cancelPendingEvent(ev1);
}).toThrow();
}).respond(400); // fail the first message
// wait for the localecho of ev1 to be updated
const p3 = new Promise<void>((resolve, reject) => {
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
room!.on(RoomEvent.LocalEchoUpdated, (ev0) => {
if (ev0 === ev1) {
resolve();
}
@@ -121,7 +127,7 @@ describe("MatrixClient retrying", function() {
expect(tl.length).toEqual(1);
// cancel the first message
client.cancelPendingEvent(ev1);
client!.cancelPendingEvent(ev1);
expect(ev1.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(0);
});
@@ -129,7 +135,7 @@ describe("MatrixClient retrying", function() {
return Promise.all([
p1,
p3,
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
});
@@ -1,16 +1,35 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import { RoomEvent } from "../../src";
import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() {
let client = null;
let httpBackend = null;
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
});
@@ -55,8 +74,7 @@ describe("MatrixClient room timelines", function() {
},
};
function setNextSyncData(events) {
events = events || [];
function setNextSyncData(events: Partial<IEvent>[] = []) {
NEXT_SYNC_DATA = {
next_batch: "n",
presence: { events: [] },
@@ -77,19 +95,9 @@ describe("MatrixClient room timelines", function() {
throw new Error("setNextSyncData only works with one room id");
}
if (e.state_key) {
if (e.__prev_event === undefined) {
throw new Error(
"setNextSyncData needs the prev state set to '__prev_event' " +
"for " + e.type,
);
}
if (e.__prev_event !== null) {
// push the previous state for this event type
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
}
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
} else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
} else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
@@ -97,7 +105,7 @@ describe("MatrixClient room timelines", function() {
});
}
beforeEach(async function() {
const setupTestClient = (): [MatrixClient, HttpBackend] => {
// these tests should work with or without timelineSupport
const testClient = new TestClient(
userId,
@@ -106,112 +114,117 @@ describe("MatrixClient room timelines", function() {
undefined,
{ timelineSupport: true },
);
httpBackend = testClient.httpBackend;
client = testClient.client;
const httpBackend = testClient.httpBackend;
const client = testClient.client;
setNextSyncData();
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, function() {
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
client.startClient();
client!.startClient();
return [client!, httpBackend];
};
beforeEach(async function() {
[client!, httpBackend] = setupTestClient();
await httpBackend.flush("/versions");
await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter");
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
httpBackend!.verifyNoOutstandingExpectation();
client!.stopClient();
return httpBackend!.stop();
});
describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client.sendTextMessage(roomId, "I am a fish", "txn1");
client!.sendTextMessage(roomId, "I am a fish", "txn1");
// check it was added
expect(room.timeline.length).toEqual(2);
// check status
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
// check member
const member = room.timeline[1].sender;
expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName);
expect(member?.userId).toEqual(userId);
expect(member?.name).toEqual(userName);
httpBackend.flush("/sync", 1).then(function() {
httpBackend!.flush("/sync", 1).then(function() {
done();
});
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"BEFORE the event comes down the event stream", function(done) {
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
msg: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
client.sendTextMessage(roomId, "I am a fish", "txn1").then(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend.flush("/sync", 1).then(function() {
const room = client!.getRoom(roomId)!;
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
httpBackend!.flush("/sync", 1).then(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
});
httpBackend.flush("/txn1", 1);
httpBackend!.flush("/txn1", 1);
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream", function(done) {
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
msg: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]);
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/sync", 1).then(function() {
const room = client!.getRoom(roomId)!;
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend!.flush("/sync", 1).then(function() {
expect(room.timeline.length).toEqual(2);
httpBackend.flush("/txn1", 1);
httpBackend!.flush("/txn1", 1);
promise.then(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId);
@@ -219,7 +232,7 @@ describe("MatrixClient room timelines", function() {
});
});
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
});
@@ -229,7 +242,7 @@ describe("MatrixClient room timelines", function() {
beforeEach(function() {
sbEvents = [];
httpBackend.when("GET", "/messages").respond(200, function() {
httpBackend!.when("GET", "/messages").respond(200, function() {
return {
chunk: sbEvents,
start: "pagin_start",
@@ -240,26 +253,26 @@ describe("MatrixClient room timelines", function() {
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() {
client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBe(null);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
@@ -275,7 +288,7 @@ describe("MatrixClient room timelines", function() {
// make an m.room.member event for alice's join
const joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice",
url: null,
url: undefined,
});
// make an m.room.member event with prev_content for alice's nick
@@ -286,7 +299,7 @@ describe("MatrixClient room timelines", function() {
});
oldMshipEvent.prev_content = {
displayname: "Old Alice",
avatar_url: null,
avatar_url: undefined,
membership: "join",
};
@@ -303,32 +316,32 @@ describe("MatrixClient room timelines", function() {
joinMshipEvent,
];
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
// sync response
expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() {
client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(5);
const joinMsg = room.timeline[0];
expect(joinMsg.sender.name).toEqual("Old Alice");
expect(joinMsg.sender?.name).toEqual("Old Alice");
const oldMsg = room.timeline[1];
expect(oldMsg.sender.name).toEqual("Old Alice");
expect(oldMsg.sender?.name).toEqual("Old Alice");
const newMsg = room.timeline[3];
expect(newMsg.sender.name).toEqual(userName);
expect(newMsg.sender?.name).toEqual(userName);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should add it them to the right place in the timeline", function(done) {
@@ -342,27 +355,27 @@ describe("MatrixClient room timelines", function() {
}),
];
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() {
client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
it("should use 'end' as the next pagination token", function(done) {
@@ -373,25 +386,25 @@ describe("MatrixClient room timelines", function() {
}),
];
client.on("sync", function(state) {
client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
expect(room.oldState.paginationToken).toBeTruthy();
client.scrollback(room, 1).then(function() {
client!.scrollback(room, 1).then(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend.flush("/messages", 1).then(function() {
httpBackend!.flush("/messages", 1).then(function() {
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
httpBackend!.flush("/sync", 1).then(() => {
done();
});
});
});
httpBackend.flush("/sync", 1);
httpBackend!.flush("/sync", 1);
});
});
@@ -404,23 +417,23 @@ describe("MatrixClient room timelines", function() {
setNextSyncData(eventData);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
let index = 0;
client.on("Room.timeline", function(event, rm, toStart) {
client!.on(RoomEvent.Timeline, function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
expect(event.event).toEqual(eventData[index]);
index += 1;
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(index).toEqual(2);
expect(room.timeline.length).toEqual(3);
@@ -442,22 +455,21 @@ describe("MatrixClient room timelines", function() {
}),
utils.mkMessage({ user: userId, room: roomId }),
];
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
expect(preNameEvent.sender?.name).toEqual(userName);
expect(postNameEvent.sender?.name).toEqual("New Name");
});
});
});
@@ -468,22 +480,21 @@ describe("MatrixClient room timelines", function() {
name: "Room 2",
},
});
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
let nameEmitCount = 0;
client.on("Room.name", function(rm) {
client!.on(RoomEvent.Name, function(rm) {
nameEmitCount += 1;
});
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
@@ -493,12 +504,11 @@ describe("MatrixClient room timelines", function() {
name: "Room 3",
},
});
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]);
}).then(function() {
expect(nameEmitCount).toEqual(2);
@@ -518,26 +528,24 @@ describe("MatrixClient room timelines", function() {
user: userC, room: roomId, mship: "invite", skey: userD,
}),
];
eventData[0].__prev_event = null;
eventData[1].__prev_event = null;
setNextSyncData(eventData);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
expect(room.currentState.getMember(userC)!.name).toEqual("C");
expect(room.currentState.getMember(userC)!.membership).toEqual(
"join",
);
expect(room.currentState.getMember(userD).name).toEqual(userD);
expect(room.currentState.getMember(userD).membership).toEqual(
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
expect(room.currentState.getMember(userD)!.membership).toEqual(
"invite",
);
});
@@ -554,26 +562,26 @@ describe("MatrixClient room timelines", function() {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend.flush("/versions", 1),
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/versions", 1),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId).name).toEqual(userName);
expect(room.currentState.getMember(userId).membership).toEqual(
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
expect(room.currentState.getMember(userId)!.membership).toEqual(
"join",
);
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId).membership).toEqual(
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(
"join",
);
});
@@ -588,21 +596,21 @@ describe("MatrixClient room timelines", function() {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(() => {
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
let emitCount = 0;
client.on("Room.timelineReset", function(emitRoom) {
client!.on(RoomEvent.TimelineReset, function(emitRoom) {
expect(emitRoom).toEqual(room);
emitCount++;
});
httpBackend.flush("/messages", 1);
httpBackend!.flush("/messages", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!),
]).then(function() {
expect(emitCount).toEqual(1);
});
@@ -618,7 +626,7 @@ describe("MatrixClient room timelines", function() {
];
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
`${encodeURIComponent(initialSyncEventData[2].event_id)}`;
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
const contextResponse = {
start: "start_token",
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
@@ -636,19 +644,19 @@ describe("MatrixClient room timelines", function() {
// Create a room from the sync
await Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 1),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Get the room after the first sync so the room is created
room = client.getRoom(roomId);
room = client!.getRoom(roomId)!;
expect(room).toBeTruthy();
});
it('should clear and refresh messages in timeline', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from.
httpBackend.when("GET", contextUrl)
httpBackend!.when("GET", contextUrl)
.respond(200, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
@@ -659,7 +667,7 @@ describe("MatrixClient room timelines", function() {
// Refresh the timeline.
await Promise.all([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
// Make sure the message are visible
@@ -681,7 +689,7 @@ describe("MatrixClient room timelines", function() {
// middle of all of this refresh timeline logic. We want to make
// sure the sync pagination still works as expected after messing
// the refresh timline logic messes with the pagination tokens.
httpBackend.when("GET", contextUrl)
httpBackend!.when("GET", contextUrl)
.respond(200, () => {
// Now finally return and make the `/context` request respond
return contextResponse;
@@ -700,7 +708,7 @@ describe("MatrixClient room timelines", function() {
const racingSyncEventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => {
const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => {
let eventFired = false;
// Throw a more descriptive error if this part of the test times out.
const failTimeout = setTimeout(() => {
@@ -726,12 +734,12 @@ describe("MatrixClient room timelines", function() {
// Then make a `/sync` happen by sending a message and seeing that it
// shows up (simulate a /sync naturally racing with us).
setNextSyncData(racingSyncEventData);
httpBackend.when("GET", "/sync").respond(200, function() {
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client, 1),
httpBackend!.flush("/sync", 1),
utils.syncPromise(client!, 1),
]);
// Make sure the timeline has the racey sync data
const afterRaceySyncTimelineEvents = room
@@ -761,7 +769,7 @@ describe("MatrixClient room timelines", function() {
await Promise.all([
refreshLiveTimelinePromise,
// Then flush the remaining `/context` to left the refresh logic complete
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
// Make sure sync pagination still works by seeing a new message show up
@@ -770,12 +778,12 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData);
httpBackend.when("GET", "/sync").respond(200, function() {
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 1),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Make sure the timeline includes the the events from the `/sync`
@@ -794,22 +802,19 @@ describe("MatrixClient room timelines", function() {
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from.
httpBackend.when("GET", contextUrl)
.respond(500, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
return {
errcode: 'TEST_FAKE_ERROR',
error: 'We purposely intercepted this /context request to make it fail ' +
'in order to test whether the refresh timeline code is resilient',
};
});
httpBackend!.when("GET", contextUrl).check(() => {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
}).respond(500, new MatrixError({
errcode: 'TEST_FAKE_ERROR',
error: 'We purposely intercepted this /context request to make it fail ' +
'in order to test whether the refresh timeline code is resilient',
}));
// Refresh the timeline and expect it to fail
const settledFailedRefreshPromises = await Promise.allSettled([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
// We only expect `TEST_FAKE_ERROR` here. Anything else is
// unexpected and should fail the test.
@@ -825,7 +830,7 @@ describe("MatrixClient room timelines", function() {
// `/messages` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` to construct a new timeline from.
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
.respond(200, function() {
return {
chunk: [{
@@ -837,7 +842,7 @@ describe("MatrixClient room timelines", function() {
// `/context` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
// timeline from.
httpBackend.when("GET", contextUrl)
httpBackend!.when("GET", contextUrl)
.respond(200, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
@@ -848,7 +853,7 @@ describe("MatrixClient room timelines", function() {
// Refresh the timeline again but this time it should pass
await Promise.all([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
httpBackend!.flushAllExpected(),
]);
// Make sure sync pagination still works by seeing a new message show up
@@ -857,12 +862,12 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData);
httpBackend.when("GET", "/sync").respond(200, function() {
httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 1),
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 1),
]);
// Make sure the message are visible
+152 -61
View File
@@ -16,7 +16,6 @@ limitations under the License.
import 'fake-indexeddb/auto';
import { Optional } from "matrix-events-sdk/lib/types";
import HttpBackend from "matrix-mock-request";
import {
@@ -29,13 +28,18 @@ import {
MatrixClient,
ClientEvent,
IndexedDBCryptoStore,
ISyncResponse,
IRoomEvent,
IJoinedRoom,
IStateEvent,
IMinimalEvent,
NotificationCountType,
} from "../../src";
import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync';
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient syncing", () => {
let client: Optional<MatrixClient> = null;
let httpBackend: Optional<HttpBackend> = null;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const otherUserId = "@bob:localhost";
@@ -44,14 +48,21 @@ describe("MatrixClient syncing", () => {
const userC = "@claire:bar";
const roomOne = "!foo:localhost";
const roomTwo = "!bar:localhost";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
beforeEach(() => {
const setupTestClient = (): [MatrixClient, HttpBackend] => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
const httpBackend = testClient.httpBackend;
const client = testClient.client;
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
return [client, httpBackend];
};
beforeEach(() => {
[client, httpBackend] = setupTestClient();
});
afterEach(() => {
@@ -80,7 +91,7 @@ describe("MatrixClient syncing", () => {
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", (done) => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
httpBackend!.when("GET", "/sync").check((req) => {
expect(req.queryParams.since).toEqual(syncData.next_batch);
expect(req.queryParams!.since).toEqual(syncData.next_batch);
}).respond(200, syncData);
client!.startClient();
@@ -91,7 +102,7 @@ describe("MatrixClient syncing", () => {
});
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
await client.initCrypto();
await client!.initCrypto();
const roomId = "!cycles:example.org";
@@ -202,7 +213,7 @@ describe("MatrixClient syncing", () => {
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
httpBackend!.when("GET", "/sync").check((req) => {
expect(JSON.parse(req.queryParams.filter).room.state.lazy_load_members).toBeTruthy();
expect(JSON.parse(req.queryParams!.filter).room.state.lazy_load_members).toBeTruthy();
}).respond(200, syncData);
client!.setGuest(false);
@@ -217,7 +228,7 @@ describe("MatrixClient syncing", () => {
client!.doesServerSupportLazyLoading = jest.fn().mockResolvedValue(true);
httpBackend!.when("GET", "/sync").check((req) => {
expect(JSON.parse(req.queryParams.filter).room?.state?.lazy_load_members).toBeFalsy();
expect(JSON.parse(req.queryParams!.filter).room?.state?.lazy_load_members).toBeFalsy();
}).respond(200, syncData);
client!.setGuest(true);
@@ -263,6 +274,16 @@ describe("MatrixClient syncing", () => {
expect(fires).toBe(1);
});
it("should work when all network calls fail", async () => {
httpBackend!.expectedRequests = [];
httpBackend!.when("GET", "").fail(0, new Error("CORS or something"));
const prom = client!.startClient();
await Promise.all([
expect(prom).resolves.toBeUndefined(),
httpBackend!.flushAllExpected(),
]);
});
});
describe("initial sync", () => {
@@ -275,11 +296,11 @@ describe("MatrixClient syncing", () => {
it("should only apply initialSyncLimit to the initial sync", () => {
// 1st request
httpBackend!.when("GET", "/sync").check((req) => {
expect(JSON.parse(req.queryParams.filter).room.timeline.limit).toEqual(1);
expect(JSON.parse(req.queryParams!.filter).room.timeline.limit).toEqual(1);
}).respond(200, syncData);
// 2nd request
httpBackend!.when("GET", "/sync").check((req) => {
expect(req.queryParams.filter).toEqual("a filter id");
expect(req.queryParams!.filter).toEqual("a filter id");
}).respond(200, syncData);
client!.startClient({ initialSyncLimit: 1 });
@@ -290,7 +311,7 @@ describe("MatrixClient syncing", () => {
it("should not apply initialSyncLimit to a first sync if we have a stored token", () => {
httpBackend!.when("GET", "/sync").check((req) => {
expect(req.queryParams.filter).toEqual("a filter id");
expect(req.queryParams!.filter).toEqual("a filter id");
}).respond(200, syncData);
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
@@ -301,26 +322,29 @@ describe("MatrixClient syncing", () => {
});
describe("resolving invites to profile info", () => {
const syncData = {
const syncData: ISyncResponse = {
account_data: {
events: [],
},
next_batch: "s_5_3",
presence: {
events: [],
},
rooms: {
join: {
},
join: {},
invite: {},
leave: {},
},
};
beforeEach(() => {
syncData.presence.events = [];
syncData.presence!.events = [];
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
}) as IRoomEvent,
],
},
state: {
@@ -339,14 +363,14 @@ describe("MatrixClient syncing", () => {
}),
],
},
};
} as unknown as IJoinedRoom;
});
it("should resolve incoming invites from /sync", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
}) as IStateEvent,
);
httpBackend!.when("GET", "/sync").respond(200, syncData);
@@ -365,26 +389,26 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(),
]).then(() => {
const member = client!.getRoom(roomOne).getMember(userC);
const member = client!.getRoom(roomOne)!.getMember(userC)!;
expect(member.name).toEqual("The Boss");
expect(
member.getAvatarUrl("home.server.url", null, null, null, false, false),
member.getAvatarUrl("home.server.url", 1, 1, '', false, false),
).toBeTruthy();
});
});
it("should use cached values from m.presence wherever possible", () => {
syncData.presence.events = [
syncData.presence!.events = [
utils.mkPresence({
user: userC,
presence: "online",
name: "The Ghost",
}),
}) as IMinimalEvent,
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
}) as IStateEvent,
);
httpBackend!.when("GET", "/sync").respond(200, syncData);
@@ -397,28 +421,28 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(),
]).then(() => {
const member = client!.getRoom(roomOne).getMember(userC);
const member = client!.getRoom(roomOne)!.getMember(userC)!;
expect(member.name).toEqual("The Ghost");
});
});
it("should result in events on the room member firing", () => {
syncData.presence.events = [
syncData.presence!.events = [
utils.mkPresence({
user: userC,
presence: "online",
name: "The Ghost",
}),
}) as IMinimalEvent,
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
}) as IStateEvent,
);
httpBackend!.when("GET", "/sync").respond(200, syncData);
let latestFiredName = null;
let latestFiredName: string;
client!.on(RoomMemberEvent.Name, (event, m) => {
if (m.userId === userC && m.roomId === roomOne) {
latestFiredName = m.name;
@@ -441,7 +465,7 @@ describe("MatrixClient syncing", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
}) as IStateEvent,
);
httpBackend!.when("GET", "/sync").respond(200, syncData);
@@ -452,10 +476,10 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(),
]).then(() => {
const member = client!.getRoom(roomOne).getMember(userC);
const member = client!.getRoom(roomOne)!.getMember(userC)!;
expect(member.name).toEqual(userC);
expect(
member.getAvatarUrl("home.server.url", null, null, null, false, false),
member.getAvatarUrl("home.server.url", 1, 1, '', false, false),
).toBe(null);
});
});
@@ -487,8 +511,8 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(),
]).then(() => {
expect(client!.getUser(userA).presence).toEqual("online");
expect(client!.getUser(userB).presence).toEqual("unavailable");
expect(client!.getUser(userA)!.presence).toEqual("online");
expect(client!.getUser(userB)!.presence).toEqual("unavailable");
});
});
});
@@ -609,7 +633,7 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(2),
]).then(() => {
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
// should have clobbered the name to the one from /events
expect(room.name).toEqual(
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
@@ -627,7 +651,7 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(2),
]).then(() => {
const room = client!.getRoom(roomTwo);
const room = client!.getRoom(roomTwo)!;
// should have added the message from /events
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getContent().body).toEqual(msgText);
@@ -643,7 +667,7 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(2),
]).then(() => {
const room = client!.getRoom(roomTwo);
const room = client!.getRoom(roomTwo)!;
// should use the display name of the other person.
expect(room.name).toEqual(otherDisplayName);
});
@@ -659,11 +683,11 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(2),
]).then(() => {
const room = client!.getRoom(roomTwo);
let member = room.getMember(otherUserId);
const room = client!.getRoom(roomTwo)!;
let member = room.getMember(otherUserId)!;
expect(member).toBeTruthy();
expect(member.typing).toEqual(true);
member = room.getMember(selfUserId);
member = room.getMember(selfUserId)!;
expect(member).toBeTruthy();
expect(member.typing).toEqual(false);
});
@@ -682,7 +706,7 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(2),
]).then(() => {
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
const stateAtStart = room.getLiveTimeline().getState(
EventTimeline.BACKWARDS,
);
@@ -780,7 +804,7 @@ describe("MatrixClient syncing", () => {
awaitSyncEvent(2),
]);
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
expect(room.getTimelineNeedsRefresh()).toEqual(false);
});
@@ -850,7 +874,7 @@ describe("MatrixClient syncing", () => {
awaitSyncEvent(),
]);
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
expect(room.getTimelineNeedsRefresh()).toEqual(false);
});
@@ -880,7 +904,7 @@ describe("MatrixClient syncing", () => {
awaitSyncEvent(),
]);
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
expect(room.getTimelineNeedsRefresh()).toEqual(false);
});
@@ -913,7 +937,7 @@ describe("MatrixClient syncing", () => {
awaitSyncEvent(),
]);
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
expect(room.getTimelineNeedsRefresh()).toEqual(false);
});
@@ -947,7 +971,7 @@ describe("MatrixClient syncing", () => {
]);
// Get the room after the first sync so the room is created
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
let emitCount = 0;
room.on(RoomEvent.HistoryImportedWithinTimeline, (markerEvent, room) => {
@@ -1003,7 +1027,7 @@ describe("MatrixClient syncing", () => {
awaitSyncEvent(2),
]);
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
expect(room.getTimelineNeedsRefresh()).toEqual(true);
});
});
@@ -1058,7 +1082,7 @@ describe("MatrixClient syncing", () => {
]);
// Get the room after the first sync so the room is created
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
expect(room).toBeTruthy();
let stateEventEmitCount = 0;
@@ -1132,7 +1156,7 @@ describe("MatrixClient syncing", () => {
]);
// Get the room after the first sync so the room is created
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
expect(room).toBeTruthy();
let stateEventEmitCount = 0;
@@ -1229,7 +1253,7 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(),
]).then(() => {
const room = client!.getRoom(roomTwo);
const room = client!.getRoom(roomTwo)!;
expect(room).toBeTruthy();
const tok = room.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
@@ -1262,9 +1286,9 @@ describe("MatrixClient syncing", () => {
client!.on(RoomEvent.TimelineReset, (room) => {
resetCallCount++;
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(0);
const tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
const tl = room?.getLiveTimeline();
expect(tl?.getEvents().length).toEqual(0);
const tok = tl?.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("newerTok");
});
@@ -1272,7 +1296,7 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(),
]).then(() => {
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
expect(resetCallCount).toEqual(1);
@@ -1351,7 +1375,7 @@ describe("MatrixClient syncing", () => {
httpBackend!.flushAllExpected(),
awaitSyncEvent(),
]).then(() => {
const room = client!.getRoom(roomOne);
const room = client!.getRoom(roomOne)!;
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
type: "m.read",
userId: userC,
@@ -1363,6 +1387,73 @@ describe("MatrixClient syncing", () => {
});
});
describe("unread notifications", () => {
const THREAD_ID = "$ThisIsARandomEventId";
const syncData = {
rooms: {
join: {
[roomOne]: {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
},
},
},
};
it("should sync unread notifications.", () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
"highlight_count": 2,
"notification_count": 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
return Promise.all([
httpBackend!.flushAllExpected(),
awaitSyncEvent(),
]).then(() => {
const room = client!.getRoom(roomOne);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2);
});
});
});
describe("of a room", () => {
xit("should sync when a join event (which changes state) for the user" +
" arrives down the event stream (e.g. join from another device)", () => {
@@ -1400,7 +1491,7 @@ describe("MatrixClient syncing", () => {
const prom = new Promise<void>((resolve) => {
httpBackend!.when("GET", "/sync").check((req) => {
expect(req.queryParams.filter).toEqual("another_id");
expect(req.queryParams!.filter).toEqual("another_id");
resolve();
}).respond(200, {});
});
@@ -1445,7 +1536,7 @@ describe("MatrixClient syncing", () => {
return Promise.all([
client!.syncLeftRooms().then(() => {
const room = client!.getRoom(roomTwo);
const room = client!.getRoom(roomTwo)!;
const tok = room.getLiveTimeline().getPaginationToken(
EventTimeline.BACKWARDS);
@@ -1467,7 +1558,7 @@ describe("MatrixClient syncing", () => {
* @returns {Promise} promise which resolves after the sync events have happened
*/
function awaitSyncEvent(numSyncs?: number) {
return utils.syncPromise(client, numSyncs);
return utils.syncPromise(client!, numSyncs);
}
});
@@ -1491,7 +1582,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
const idbHttpBackend = idbTestClient.httpBackend;
const idbClient = idbTestClient.client;
idbHttpBackend.when("GET", "/versions").respond(200, {});
idbHttpBackend.when("GET", "/pushrules").respond(200, {});
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
await idbClient.initCrypto();
+21 -16
View File
@@ -95,26 +95,31 @@ describe("megolm key backups", function() {
return;
}
const Olm = global.Olm;
let testOlmAccount: Account;
let testOlmAccount: Olm.Account;
let aliceTestClient: TestClient;
const setupTestClient = (): [Account, TestClient] => {
const aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
const testOlmAccount = new Olm.Account();
testOlmAccount!.create();
return [testOlmAccount, aliceTestClient];
};
beforeAll(function() {
return Olm.init();
});
beforeEach(async function() {
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
testOlmAccount = new Olm.Account();
testOlmAccount.create();
await aliceTestClient.client.initCrypto();
aliceTestClient.client.crypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
[testOlmAccount, aliceTestClient] = setupTestClient();
await aliceTestClient!.client.initCrypto();
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
});
afterEach(function() {
return aliceTestClient.stop();
return aliceTestClient!.stop();
});
it("Alice checks key backups when receiving a message she can't decrypt", function() {
@@ -130,22 +135,22 @@ describe("megolm key backups", function() {
},
};
return aliceTestClient.start().then(() => {
return aliceTestClient!.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then(() => {
const privkey = decodeRecoveryKey(RECOVERY_KEY);
return aliceTestClient.client.crypto.storeSessionBackupPrivateKey(privkey);
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
}).then(() => {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient.expectKeyBackupQuery(
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient!.expectKeyBackupQuery(
ROOM_ID,
SESSION_ID,
200,
CURVE25519_KEY_BACKUP_DATA,
);
return aliceTestClient.httpBackend.flushAllExpected();
return aliceTestClient!.httpBackend.flushAllExpected();
}).then(function(): Promise<MatrixEvent> {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
if (event.getContent()) {
+64 -60
View File
@@ -207,9 +207,11 @@ describe("megolm", () => {
}
const Olm = global.Olm;
let testOlmAccount: Olm.Account;
let testSenderKey: string;
let aliceTestClient: TestClient;
let testOlmAccount = {} as unknown as Olm.Account;
let testSenderKey = '';
let aliceTestClient = new TestClient(
"@alice:localhost", "device2", "access_token2",
);
/**
* Get the device keys for testOlmAccount in a format suitable for a
@@ -283,12 +285,12 @@ describe("megolm", () => {
it("Alice receives a megolm message", async () => {
await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
@@ -322,7 +324,7 @@ describe("megolm", () => {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(event);
@@ -332,12 +334,12 @@ describe("megolm", () => {
it("Alice receives a megolm message before the session keys", async () => {
// https://github.com/vector-im/element-web/issues/2273
await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event, but don't send it yet
const roomKeyEncrypted = encryptGroupSessionKey({
@@ -362,7 +364,7 @@ describe("megolm", () => {
});
await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
expect(room.getLiveTimeline().getEvents()[0].getContent().msgtype).toEqual('m.bad.encrypted');
// now she gets the room_key event
@@ -392,12 +394,12 @@ describe("megolm", () => {
it("Alice gets a second room_key message", async () => {
await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event
const roomKeyEncrypted1 = encryptGroupSessionKey({
@@ -451,7 +453,7 @@ describe("megolm", () => {
await aliceTestClient.flushSync();
await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
await room.decryptCriticalEvents();
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
@@ -499,7 +501,7 @@ describe("megolm", () => {
let inboundGroupSession: Olm.InboundGroupSession;
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(_path, content) {
).respond(200, function(_path, content: any) {
const m = content.messages['@bob:xyz'].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
@@ -525,7 +527,7 @@ describe("megolm", () => {
return { event_id: '$event_id' };
});
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const pendingMsg = room.getPendingEvents()[0];
await Promise.all([
@@ -628,7 +630,7 @@ describe("megolm", () => {
let megolmSessionId: string;
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(_path, content) {
).respond(200, function(_path, content: any) {
logger.log('sendToDevice: ', content);
const m = content.messages['@bob:xyz'].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
@@ -706,7 +708,7 @@ describe("megolm", () => {
// invalidate the device cache for all members in e2e rooms (ie,
// herself), and do a key query.
aliceTestClient.expectKeyQuery(
getTestKeysQueryResponse(aliceTestClient.userId),
getTestKeysQueryResponse(aliceTestClient.userId!),
);
await aliceTestClient.httpBackend.flushAllExpected();
@@ -716,28 +718,30 @@ describe("megolm", () => {
await aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
throw new Error("sendTextMessage succeeded on an unknown device");
} catch (e) {
expect(e.name).toEqual("UnknownDeviceError");
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
expect(Object.keys(e.devices[aliceTestClient.userId])).
expect((e as any).name).toEqual("UnknownDeviceError");
expect(Object.keys((e as any).devices)).toEqual([aliceTestClient.userId!]);
expect(Object.keys((e as any)?.devices[aliceTestClient.userId!])).
toEqual(['DEVICE_ID']);
}
// mark the device as known, and resend.
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID');
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId!, 'DEVICE_ID');
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
200, function(_path, content) {
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
200, function(_path, content: IClaimOTKsResult) {
expect(content.one_time_keys[aliceTestClient.userId!].DEVICE_ID)
.toEqual("signed_curve25519");
return getTestKeysClaimResponse(aliceTestClient.userId);
return getTestKeysClaimResponse(aliceTestClient.userId!);
});
let p2pSession: Olm.Session;
let inboundGroupSession: Olm.InboundGroupSession;
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(_path, content) {
).respond(200, function(_path, content: {
messages: { [userId: string]: { [deviceId: string]: Record<string, any> }};
}) {
logger.log("sendToDevice: ", content);
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
const m = content.messages[aliceTestClient.userId!].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(0); // pre-key message
@@ -751,7 +755,7 @@ describe("megolm", () => {
return {};
});
let decrypted: IEvent;
let decrypted: Partial<IEvent> = {};
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(_path, content: IContent) {
@@ -766,7 +770,7 @@ describe("megolm", () => {
});
// Grab the event that we'll need to resend
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const pendingEvents = room.getPendingEvents();
expect(pendingEvents.length).toEqual(1);
const unsentEvent = pendingEvents[0];
@@ -781,7 +785,7 @@ describe("megolm", () => {
]);
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
expect(decrypted.content?.body).toEqual('test');
});
it('Alice should wait for device list to complete when sending a megolm message', async () => {
@@ -830,11 +834,11 @@ describe("megolm", () => {
it("Alice exports megolm keys and imports them to a new device", async () => {
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
// establish an olm session with alice
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
@@ -867,7 +871,7 @@ describe("megolm", () => {
});
await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
await room.decryptCriticalEvents();
expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42');
@@ -883,7 +887,7 @@ describe("megolm", () => {
await aliceTestClient.client.importRoomKeys(exported);
await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
const syncResponse = {
next_batch: 1,
@@ -927,7 +931,7 @@ describe("megolm", () => {
...rawEvent,
room: ROOM_ID,
});
await event1.attemptDecryption(testClient.client.crypto, { isRetry: true });
await event1.attemptDecryption(testClient.client.crypto!, { isRetry: true });
expect(event1.isKeySourceUntrusted()).toBeTruthy();
const event2 = testUtils.mkEvent({
@@ -943,26 +947,26 @@ describe("megolm", () => {
// @ts-ignore - private
event2.senderCurve25519Key = testSenderKey;
// @ts-ignore - private
testClient.client.crypto.onRoomKeyEvent(event2);
testClient.client.crypto!.onRoomKeyEvent(event2);
const event3 = testUtils.mkEvent({
event: true,
...rawEvent,
room: ROOM_ID,
});
await event3.attemptDecryption(testClient.client.crypto, { isRetry: true });
await event3.attemptDecryption(testClient.client.crypto!, { isRetry: true });
expect(event3.isKeySourceUntrusted()).toBeFalsy();
testClient.stop();
});
it("Alice can decrypt a message with falsey content", async () => {
await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
@@ -1005,7 +1009,7 @@ describe("megolm", () => {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(event);
@@ -1018,12 +1022,12 @@ describe("megolm", () => {
"should successfully decrypt bundled redaction events that don't include a room_id in their /sync data",
async () => {
await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
@@ -1072,10 +1076,10 @@ describe("megolm", () => {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
await event.attemptDecryption(aliceTestClient.client.crypto);
await event.attemptDecryption(aliceTestClient.client.crypto!);
expect(event.getContent()).toEqual({});
const redactionEvent: any = event.getRedactionEvent();
expect(redactionEvent.content.reason).toEqual("redaction test");
@@ -1089,7 +1093,7 @@ describe("megolm", () => {
await beccaTestClient.client.initCrypto();
await aliceTestClient.start();
aliceTestClient.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await beccaTestClient.start();
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
@@ -1107,7 +1111,7 @@ describe("megolm", () => {
},
});
await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
@@ -1116,23 +1120,23 @@ describe("megolm", () => {
// @ts-ignore private properties
event.claimedEd25519Key = null;
const device = new DeviceInfo(beccaTestClient.client.deviceId);
aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device;
aliceTestClient.client.crypto.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId();
const device = new DeviceInfo(beccaTestClient.client.deviceId!);
aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId()!;
// Create an olm session for Becca and Alice's devices
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
await beccaTestClient.client.crypto.cryptoStore.doTxn(
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
} finally {
account.free();
@@ -1142,7 +1146,7 @@ describe("megolm", () => {
);
const content = event.getWireContent();
const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
ROOM_ID,
content.sender_key,
content.session_id,
@@ -1213,7 +1217,7 @@ describe("megolm", () => {
});
await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const roomEvent = room.getLiveTimeline().getEvents()[0];
expect(roomEvent.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
@@ -1246,7 +1250,7 @@ describe("megolm", () => {
},
});
await beccaTestClient.client.crypto.encryptEvent(event, beccaRoom);
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
@@ -1255,22 +1259,22 @@ describe("megolm", () => {
// @ts-ignore private properties
event.claimedEd25519Key = null;
const device = new DeviceInfo(beccaTestClient.client.deviceId);
aliceTestClient.client.crypto.deviceList.getDeviceByIdentityKey = () => device;
const device = new DeviceInfo(beccaTestClient.client.deviceId!);
aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
// Create an olm session for Becca and Alice's devices
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
await beccaTestClient.client.crypto.cryptoStore.doTxn(
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto.cryptoStore.getAccount(txn, (pickledAccount: string) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto.olmDevice.pickleKey, pickledAccount);
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
} finally {
account.free();
@@ -1280,7 +1284,7 @@ describe("megolm", () => {
);
const content = event.getWireContent();
const groupSessionKey = await beccaTestClient.client.crypto.olmDevice.getInboundGroupSessionKey(
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
ROOM_ID,
content.sender_key,
content.session_id,
@@ -1352,7 +1356,7 @@ describe("megolm", () => {
await aliceTestClient.flushSync();
// Decryption should fail, because Alice hasn't received any keys she can trust
const room = aliceTestClient.client.getRoom(ROOM_ID);
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const roomEvent = room.getLiveTimeline().getEvents()[0];
expect(roomEvent.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
+93 -91
View File
@@ -23,18 +23,19 @@ import { TestClient } from "../TestClient";
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
import {
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
} from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
describe("SlidingSyncSdk", () => {
let client: MatrixClient = null;
let httpBackend: MockHttpBackend = null;
let sdk: SlidingSyncSdk = null;
let mockSlidingSync: SlidingSync = null;
let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend | undefined;
let sdk: SlidingSyncSdk | undefined;
let mockSlidingSync: SlidingSync | undefined;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
@@ -66,7 +67,7 @@ describe("SlidingSyncSdk", () => {
event_id: "$" + eventIdCounter,
};
};
const mkOwnStateEvent = (evType: string, content: object, stateKey?: string): IStateEvent => {
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
eventIdCounter++;
return {
type: evType,
@@ -103,24 +104,24 @@ describe("SlidingSyncSdk", () => {
client = testClient.client;
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
if (testOpts.withCrypto) {
httpBackend.when("GET", "/room_keys/version").respond(404, {});
await client.initCrypto();
testOpts.crypto = client.crypto;
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto();
testOpts.crypto = client!.crypto;
}
httpBackend.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
};
// tear down client/httpBackend globals
const teardownClient = () => {
client.stopClient();
return httpBackend.stop();
client!.stopClient();
return httpBackend!.stop();
};
// find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension => {
expect(mockSlidingSync.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync.registerExtension as jest.Mock;
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;
@@ -137,14 +138,14 @@ describe("SlidingSyncSdk", () => {
});
afterAll(teardownClient);
it("can sync()", async () => {
const hasSynced = sdk.sync();
await httpBackend.flushAllExpected();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
expect(mockSlidingSync.start).toBeCalled();
expect(mockSlidingSync!.start).toBeCalled();
});
it("can stop()", async () => {
sdk.stop();
expect(mockSlidingSync.stop).toBeCalled();
sdk!.stop();
expect(mockSlidingSync!.stop).toBeCalled();
});
});
@@ -156,8 +157,8 @@ describe("SlidingSyncSdk", () => {
describe("initial", () => {
beforeAll(async () => {
const hasSynced = sdk.sync();
await httpBackend.flushAllExpected();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
});
// inject some rooms with different fields set.
@@ -277,8 +278,8 @@ describe("SlidingSyncSdk", () => {
};
it("can be created with required_state and timeline", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
const gotRoom = client.getRoom(roomA);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomA].name);
@@ -287,8 +288,8 @@ describe("SlidingSyncSdk", () => {
});
it("can be created with timeline only", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
const gotRoom = client.getRoom(roomB);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomB].name);
@@ -297,8 +298,8 @@ describe("SlidingSyncSdk", () => {
});
it("can be created with a highlight_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
const gotRoom = client.getRoom(roomC);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
@@ -307,8 +308,8 @@ describe("SlidingSyncSdk", () => {
});
it("can be created with a notification_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
const gotRoom = client.getRoom(roomD);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
@@ -317,8 +318,8 @@ describe("SlidingSyncSdk", () => {
});
it("can be created with an invited/joined_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
const gotRoom = client.getRoom(roomG);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
@@ -326,8 +327,8 @@ describe("SlidingSyncSdk", () => {
});
it("can be created with invite_state", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
const gotRoom = client.getRoom(roomE);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getMyMembership()).toEqual("invite");
@@ -335,8 +336,8 @@ describe("SlidingSyncSdk", () => {
});
it("uses the 'name' field to caluclate the room name", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
const gotRoom = client.getRoom(roomF);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
const gotRoom = client!.getRoom(roomF);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
@@ -347,12 +348,12 @@ describe("SlidingSyncSdk", () => {
describe("updating", () => {
it("can update with a new timeline event", async () => {
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
timeline: [newEvent],
required_state: [],
name: data[roomA].name,
});
const gotRoom = client.getRoom(roomA);
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
const newTimeline = data[roomA].timeline;
@@ -361,31 +362,31 @@ describe("SlidingSyncSdk", () => {
});
it("can update with a new required_state event", async () => {
let gotRoom = client.getRoom(roomB);
let gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
required_state: [
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
],
timeline: [],
name: data[roomB].name,
});
gotRoom = client.getRoom(roomB);
gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
});
it("can update with a new highlight_count", async () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, {
name: data[roomC].name,
required_state: [],
timeline: [],
highlight_count: 1,
});
const gotRoom = client.getRoom(roomC);
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
@@ -394,13 +395,13 @@ describe("SlidingSyncSdk", () => {
});
it("can update with a new notification_count", async () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, {
name: data[roomD].name,
required_state: [],
timeline: [],
notification_count: 1,
});
const gotRoom = client.getRoom(roomD);
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
@@ -409,13 +410,13 @@ describe("SlidingSyncSdk", () => {
});
it("can update with a new joined_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, {
name: data[roomD].name,
required_state: [],
timeline: [],
joined_count: 1,
});
const gotRoom = client.getRoom(roomG);
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
@@ -433,13 +434,13 @@ describe("SlidingSyncSdk", () => {
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
...timeline,
];
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
timeline: oldTimeline,
required_state: [],
name: data[roomA].name,
initial: true, // e.g requested via room subscription
});
const gotRoom = client.getRoom(roomA);
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
@@ -458,50 +459,50 @@ describe("SlidingSyncSdk", () => {
describe("lifecycle", () => {
beforeAll(async () => {
await setupClient();
const hasSynced = sdk.sync();
await httpBackend.flushAllExpected();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
});
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync.emit(
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null,
);
expect(sdk.getSyncState()).toEqual(SyncState.Syncing);
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
mockSlidingSync.emit(
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
);
expect(sdk.getSyncState()).toEqual(SyncState.Reconnecting);
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
mockSlidingSync.emit(
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
);
}
expect(sdk.getSyncState()).toEqual(SyncState.Error);
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
});
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
mockSlidingSync.emit(
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle,
SlidingSyncState.Complete,
{ pos: "i", lists: [], rooms: {}, extensions: {} },
null,
);
expect(sdk.getSyncState()).toEqual(SyncState.Syncing);
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({
expect(mockSlidingSync!.stop).not.toBeCalled();
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
errcode: "M_UNKNOWN_TOKEN",
message: "Oh no your access token is no longer valid",
}));
expect(sdk.getSyncState()).toEqual(SyncState.Error);
expect(mockSlidingSync.stop).toBeCalled();
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
expect(mockSlidingSync!.stop).toBeCalled();
});
});
@@ -517,8 +518,8 @@ describe("SlidingSyncSdk", () => {
avatar_url: "mxc://foobar",
displayname: "The Invitee",
};
httpBackend.when("GET", "/profile").respond(200, inviteeProfile);
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
httpBackend!.when("GET", "/profile").respond(200, inviteeProfile);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
initial: true,
name: "Room with Invite",
required_state: [],
@@ -529,10 +530,11 @@ describe("SlidingSyncSdk", () => {
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
],
});
await httpBackend.flush("/profile", 1, 1000);
const room = client.getRoom(roomId);
await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name);
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
const inviteeMember = room.getMember(invitee);
const inviteeMember = room.getMember(invitee)!;
expect(inviteeMember).toBeDefined();
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
@@ -545,8 +547,8 @@ describe("SlidingSyncSdk", () => {
await setupClient({
withCrypto: true,
});
const hasSynced = sdk.sync();
await httpBackend.flushAllExpected();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("e2ee");
});
@@ -554,7 +556,7 @@ describe("SlidingSyncSdk", () => {
// 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();
client!.crypto!.stop();
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
@@ -572,38 +574,38 @@ describe("SlidingSyncSdk", () => {
// TODO: more assertions?
});
it("can update OTK counts", () => {
client.crypto.updateOneTimeKeyCount = jest.fn();
client!.crypto!.updateOneTimeKeyCount = jest.fn();
ext.onResponse({
device_one_time_keys_count: {
signed_curve25519: 42,
},
});
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
ext.onResponse({
device_one_time_keys_count: {
not_signed_curve25519: 42,
// missing field -> default to 0
},
});
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
});
it("can update fallback keys", () => {
ext.onResponse({
device_unused_fallback_key_types: ["signed_curve25519"],
});
expect(client.crypto.getNeedsNewFallback()).toEqual(false);
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
ext.onResponse({
device_unused_fallback_key_types: ["not_signed_curve25519"],
});
expect(client.crypto.getNeedsNewFallback()).toEqual(true);
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
});
});
describe("ExtensionAccountData", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk.sync();
await httpBackend.flushAllExpected();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("account_data");
});
@@ -618,7 +620,7 @@ describe("SlidingSyncSdk", () => {
const globalContent = {
info: "here",
};
let globalData = client.getAccountData(globalType);
let globalData = client!.getAccountData(globalType);
expect(globalData).toBeUndefined();
ext.onResponse({
global: [
@@ -628,13 +630,13 @@ describe("SlidingSyncSdk", () => {
},
],
});
globalData = client.getAccountData(globalType);
globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeDefined();
expect(globalData.getContent()).toEqual(globalContent);
});
it("processes rooms account data", async () => {
const roomId = "!room:id";
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with account data",
required_state: [],
timeline: [
@@ -660,9 +662,9 @@ describe("SlidingSyncSdk", () => {
],
},
});
const room = client.getRoom(roomId);
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
const event = room.getAccountData(roomType);
const event = room.getAccountData(roomType)!;
expect(event).toBeDefined();
expect(event.getContent()).toEqual(roomContent);
});
@@ -681,9 +683,9 @@ describe("SlidingSyncSdk", () => {
],
},
});
const room = client.getRoom(unknownRoomId);
const room = client!.getRoom(unknownRoomId);
expect(room).toBeNull();
expect(client.getAccountData(roomType)).toBeUndefined();
expect(client!.getAccountData(roomType)).toBeUndefined();
});
it("can update push rules via account data", async () => {
const roomId = "!foo:bar";
@@ -703,7 +705,7 @@ describe("SlidingSyncSdk", () => {
}],
},
};
let pushRule = client.getRoomPushRule("global", roomId);
let pushRule = client!.getRoomPushRule("global", roomId);
expect(pushRule).toBeUndefined();
ext.onResponse({
global: [
@@ -713,16 +715,16 @@ describe("SlidingSyncSdk", () => {
},
],
});
pushRule = client.getRoomPushRule("global", roomId);
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific][0]);
pushRule = client!.getRoomPushRule("global", roomId)!;
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
});
});
describe("ExtensionToDevice", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk.sync();
await httpBackend.flushAllExpected();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("to_device");
});
@@ -753,7 +755,7 @@ describe("SlidingSyncSdk", () => {
foo: "bar",
};
let called = false;
client.once(ClientEvent.ToDeviceEvent, (ev) => {
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
expect(ev.getContent()).toEqual(toDeviceContent);
expect(ev.getType()).toEqual(toDeviceType);
called = true;
@@ -771,7 +773,7 @@ describe("SlidingSyncSdk", () => {
});
it("can cancel key verification requests", async () => {
const seen: Record<string, boolean> = {};
client.on(ClientEvent.ToDeviceEvent, (ev) => {
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
const evType = ev.getType();
expect(seen[evType]).toBeFalsy();
seen[evType] = true;
+196 -83
View File
@@ -22,7 +22,6 @@ import { SlidingSync, SlidingSyncState, ExtensionState, SlidingSyncEvent } from
import { TestClient } from "../TestClient";
import { logger } from "../../src/logger";
import { MatrixClient } from "../../src";
import { sleep } from "../../src/utils";
/**
* Tests for sliding sync. These tests are broken down into sub-tests which are reliant upon one another.
@@ -30,8 +29,8 @@ import { sleep } from "../../src/utils";
* Each test will call different functions on SlidingSync which may depend on state from previous tests.
*/
describe("SlidingSync", () => {
let client: MatrixClient = null;
let httpBackend: MockHttpBackend = null;
let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend | undefined;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const proxyBaseUrl = "http://localhost:8008";
@@ -46,9 +45,9 @@ describe("SlidingSync", () => {
// tear down client/httpBackend globals
const teardownClient = () => {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
httpBackend!.verifyNoOutstandingExpectation();
client!.stopClient();
return httpBackend!.stop();
};
describe("start/stop", () => {
@@ -57,14 +56,14 @@ describe("SlidingSync", () => {
let slidingSync: SlidingSync;
it("should start the sync loop upon calling start()", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
const fakeResp = {
pos: "a",
lists: [],
rooms: {},
extensions: {},
};
httpBackend.when("POST", syncUrl).respond(200, fakeResp);
httpBackend!.when("POST", syncUrl).respond(200, fakeResp);
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
expect(state).toEqual(SlidingSyncState.RequestFinished);
expect(resp).toEqual(fakeResp);
@@ -72,13 +71,113 @@ describe("SlidingSync", () => {
return true;
});
slidingSync.start();
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await p;
});
it("should stop the sync loop upon calling stop()", () => {
slidingSync.stop();
httpBackend.verifyNoOutstandingExpectation();
httpBackend!.verifyNoOutstandingExpectation();
});
it("should reset the connection on HTTP 400 and send everything again", async () => {
// seed the connection with some lists, extensions and subscriptions to verify they are sent again
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
const roomId = "!sub:localhost";
const subInfo = {
timeline_limit: 42,
required_state: [["m.room.create", ""]],
};
const listInfo = {
ranges: [[0, 10]],
filters: {
is_dm: true,
},
};
const ext = {
name: () => "custom_extension",
onRequest: (initial) => { return { initial: initial }; },
onResponse: (res) => { return {}; },
when: () => ExtensionState.PreProcess,
};
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
slidingSync.modifyRoomSubscriptionInfo(subInfo);
slidingSync.setList(0, listInfo);
slidingSync.registerExtension(ext);
slidingSync.start();
// expect everything to be sent
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toEqual({
[roomId]: subInfo,
});
expect(body.lists[0]).toEqual(listInfo);
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams["pos"]).toBeUndefined();
txnId = body.txn_id;
}).respond(200, function() {
return {
pos: "11",
lists: [{ count: 5 }],
extensions: {},
txn_id: txnId,
};
});
await httpBackend.flushAllExpected();
// expect nothing but ranges and non-initial extensions to be sent
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
expect(body.lists[0]).toEqual({
ranges: [[0, 10]],
});
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: false });
expect(req.queryParams["pos"]).toEqual("11");
}).respond(200, function() {
return {
pos: "12",
lists: [{ count: 5 }],
extensions: {},
};
});
await httpBackend.flushAllExpected();
// now we expire the session
httpBackend.when("POST", syncUrl).respond(400, function() {
logger.debug("sending session expired 400");
return {
error: "HTTP 400 : session expired",
};
});
await httpBackend.flushAllExpected();
// ...and everything should be sent again
httpBackend.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toEqual({
[roomId]: subInfo,
});
expect(body.lists[0]).toEqual(listInfo);
expect(body.extensions).toBeTruthy();
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
expect(req.queryParams["pos"]).toBeUndefined();
}).respond(200, function() {
return {
pos: "1",
lists: [{ count: 6 }],
extensions: {},
};
});
await httpBackend.flushAllExpected();
slidingSync.stop();
});
});
@@ -103,9 +202,9 @@ describe("SlidingSync", () => {
it("should be able to subscribe to a room", async () => {
// add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("room sub", body);
expect(body.room_subscriptions).toBeTruthy();
@@ -125,7 +224,7 @@ describe("SlidingSync", () => {
return true;
});
slidingSync.start();
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await p;
});
@@ -137,7 +236,7 @@ describe("SlidingSync", () => {
["m.room.member", "*"],
],
};
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("adjusted sub", body);
expect(body.room_subscriptions).toBeTruthy();
@@ -158,7 +257,7 @@ describe("SlidingSync", () => {
});
slidingSync.modifyRoomSubscriptionInfo(newSubInfo);
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await p;
// need to set what the new subscription info is for subsequent tests
roomSubInfo = newSubInfo;
@@ -179,7 +278,7 @@ describe("SlidingSync", () => {
required_state: [],
timeline: [],
};
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("new subs", body);
expect(body.room_subscriptions).toBeTruthy();
@@ -204,12 +303,12 @@ describe("SlidingSync", () => {
const subs = slidingSync.getRoomSubscriptions();
subs.add(anotherRoomID);
slidingSync.modifyRoomSubscriptions(subs);
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await p;
});
it("should be able to unsubscribe from a room", async () => {
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("unsub request", body);
expect(body.room_subscriptions).toBeFalsy();
@@ -226,7 +325,7 @@ describe("SlidingSync", () => {
// remove the subscription for the first room
slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID]));
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await p;
slidingSync.stop();
@@ -273,8 +372,8 @@ describe("SlidingSync", () => {
is_dm: true,
},
};
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client, 1);
httpBackend.when("POST", syncUrl).check(function(req) {
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1);
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("list", body);
expect(body.lists).toBeTruthy();
@@ -301,7 +400,7 @@ describe("SlidingSync", () => {
return state === SlidingSyncState.Complete;
});
slidingSync.start();
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
expect(listenerData[roomA]).toEqual(rooms[roomA]);
@@ -327,7 +426,7 @@ describe("SlidingSync", () => {
it("should be possible to adjust list ranges", async () => {
// modify the list ranges
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("next ranges", body.lists[0].ranges);
expect(body.lists).toBeTruthy();
@@ -351,7 +450,7 @@ describe("SlidingSync", () => {
return state === SlidingSyncState.RequestFinished;
});
slidingSync.setListRanges(0, newRanges);
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
});
@@ -364,7 +463,7 @@ describe("SlidingSync", () => {
"is_dm": true,
},
};
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("extra list", body);
expect(body.lists).toBeTruthy();
@@ -403,13 +502,13 @@ describe("SlidingSync", () => {
return state === SlidingSyncState.Complete;
});
slidingSync.setList(1, extraListReq);
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
});
it("should be possible to get list DELETE/INSERTs", async () => {
// move C (2) to A (0)
httpBackend.when("POST", syncUrl).respond(200, {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e",
lists: [{
count: 500,
@@ -440,12 +539,12 @@ describe("SlidingSync", () => {
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
// move C (0) back to A (2)
httpBackend.when("POST", syncUrl).respond(200, {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f",
lists: [{
count: 500,
@@ -476,13 +575,13 @@ describe("SlidingSync", () => {
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should ignore invalid list indexes", async () => {
httpBackend.when("POST", syncUrl).respond(200, {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e",
lists: [{
count: 500,
@@ -509,13 +608,13 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should be possible to update a list", async () => {
httpBackend.when("POST", syncUrl).respond(200, {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g",
lists: [{
count: 42,
@@ -555,7 +654,7 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
@@ -567,7 +666,7 @@ describe("SlidingSync", () => {
1: roomC,
};
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend.when("POST", syncUrl).respond(200, {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it
lists: [{
@@ -598,7 +697,7 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
@@ -608,7 +707,7 @@ describe("SlidingSync", () => {
0: roomB,
1: roomC,
});
httpBackend.when("POST", syncUrl).respond(200, {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g",
lists: [{
count: 499,
@@ -634,7 +733,7 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
});
@@ -643,7 +742,7 @@ describe("SlidingSync", () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
0: roomC,
});
httpBackend.when("POST", syncUrl).respond(200, {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [{
count: 500,
@@ -670,11 +769,11 @@ describe("SlidingSync", () => {
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
httpBackend.when("POST", syncUrl).respond(200, {
httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [{
count: 501,
@@ -702,7 +801,7 @@ describe("SlidingSync", () => {
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await responseProcessed;
await listPromise;
slidingSync.stop();
@@ -725,11 +824,11 @@ describe("SlidingSync", () => {
],
};
// add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
// modification before SlidingSync.start()
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy();
@@ -752,7 +851,7 @@ describe("SlidingSync", () => {
};
});
slidingSync.start();
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await subscribePromise;
});
it("should resolve setList during a connection", async () => {
@@ -761,7 +860,7 @@ describe("SlidingSync", () => {
};
const promise = slidingSync.setList(0, newList);
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
@@ -776,14 +875,14 @@ describe("SlidingSync", () => {
extensions: {},
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
it("should resolve setListRanges during a connection", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]);
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
@@ -800,7 +899,7 @@ describe("SlidingSync", () => {
extensions: {},
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
@@ -809,7 +908,7 @@ describe("SlidingSync", () => {
timeline_limit: 99,
});
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy();
@@ -825,22 +924,22 @@ describe("SlidingSync", () => {
extensions: {},
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await promise;
expect(txnId).toBeDefined();
});
it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
const gotTxnIds = [];
const gotTxnIds: any[] = [];
const pushTxn = function(req) {
gotTxnIds.push(req.data.txn_id);
};
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
await httpBackend.flushAllExpected();
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
await httpBackend!.flushAllExpected();
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
await httpBackend.flushAllExpected();
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
await httpBackend!.flushAllExpected();
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail.
@@ -849,7 +948,7 @@ describe("SlidingSync", () => {
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
let txnId;
httpBackend.when("POST", syncUrl).check((req) => {
httpBackend!.when("POST", syncUrl).check((req) => {
txnId = req.data.txn_id;
}).respond(200, () => {
// include the txn_id, earlier requests should now be reject()ed.
@@ -858,23 +957,23 @@ describe("SlidingSync", () => {
txn_id: txnId,
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await okPromise;
expect(txnId).toBeDefined();
});
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
const gotTxnIds = [];
const gotTxnIds: any[] = [];
const pushTxn = function(req) {
gotTxnIds.push(req.data.txn_id);
gotTxnIds.push(req.data?.txn_id);
};
const A = slidingSync.setListRanges(0, [[20, 40]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
await httpBackend.flushAllExpected();
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
await httpBackend!.flushAllExpected();
const B = slidingSync.setListRanges(0, [[60, 70]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
await httpBackend.flushAllExpected();
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
await httpBackend!.flushAllExpected();
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail.
@@ -885,14 +984,14 @@ describe("SlidingSync", () => {
C.finally(() => {
pendingC = false;
});
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, () => {
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, () => {
// include the txn_id for B, so C's promise is outstanding
return {
pos: "C",
txn_id: gotTxnIds[1],
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
// A is rejected, see above
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
expect(pendingC).toBe(true); // C is pending still
@@ -904,7 +1003,7 @@ describe("SlidingSync", () => {
pending = false;
});
let txnId;
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy();
@@ -921,7 +1020,7 @@ describe("SlidingSync", () => {
extensions: {},
};
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
expect(txnId).toBeDefined();
expect(pending).toBe(true);
slidingSync.stop();
@@ -963,10 +1062,10 @@ describe("SlidingSync", () => {
};
it("should be able to register an extension", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
slidingSync.registerExtension(extPre);
const callbackOrder = [];
const callbackOrder: string[] = [];
let extensionOnResponseCalled = false;
onPreExtensionRequest = () => {
return extReq;
@@ -977,7 +1076,7 @@ describe("SlidingSync", () => {
expect(resp).toEqual(extResp);
};
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("ext req", body);
expect(body.extensions).toBeTruthy();
@@ -998,7 +1097,7 @@ describe("SlidingSync", () => {
}
});
slidingSync.start();
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await p;
expect(extensionOnResponseCalled).toBe(true);
expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]);
@@ -1012,7 +1111,7 @@ describe("SlidingSync", () => {
onPreExtensionResponse = (resp) => {
responseCalled = true;
};
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("ext req nothing", body);
expect(body.extensions).toBeTruthy();
@@ -1030,7 +1129,7 @@ describe("SlidingSync", () => {
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await p;
expect(responseCalled).toBe(false);
});
@@ -1041,13 +1140,13 @@ describe("SlidingSync", () => {
return extReq;
};
let responseCalled = false;
const callbackOrder = [];
const callbackOrder: string[] = [];
onPostExtensionResponse = (resp) => {
expect(resp).toEqual(extResp);
responseCalled = true;
callbackOrder.push("onPostExtensionResponse");
};
httpBackend.when("POST", syncUrl).check(function(req) {
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("ext req after start", body);
expect(body.extensions).toBeTruthy();
@@ -1071,7 +1170,7 @@ describe("SlidingSync", () => {
return true;
}
});
await httpBackend.flushAllExpected();
await httpBackend!.flushAllExpected();
await p;
expect(responseCalled).toBe(true);
expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]);
@@ -1079,16 +1178,27 @@ describe("SlidingSync", () => {
});
it("is not possible to register the same extension name twice", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
slidingSync.registerExtension(extPre);
expect(() => { slidingSync.registerExtension(extPre); }).toThrow();
});
});
});
async function timeout(delayMs: number, reason: string): Promise<never> {
await sleep(delayMs);
throw new Error(`timeout: ${delayMs}ms - ${reason}`);
function timeout(delayMs: number, reason: string): { promise: Promise<never>, cancel: () => void } {
let timeoutId;
return {
promise: new Promise((resolve, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`timeout: ${delayMs}ms - ${reason}`));
}, delayMs);
}),
cancel: () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
},
};
}
/**
@@ -1106,19 +1216,22 @@ function listenUntil<T>(
callback: (...args: any[]) => T,
timeoutMs = 500,
): Promise<T> {
const trace = new Error().stack.split(`\n`)[2];
const trace = new Error().stack?.split(`\n`)[2];
const t = timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace);
return Promise.race([new Promise<T>((resolve, reject) => {
const wrapper = (...args) => {
try {
const data = callback(...args);
if (data) {
emitter.off(eventName, wrapper);
t.cancel();
resolve(data);
}
} catch (err) {
reject(err);
t.cancel();
}
};
emitter.on(eventName, wrapper);
}), timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace)]);
}), t.promise]);
}
-10
View File
@@ -16,7 +16,6 @@ limitations under the License.
*/
import { logger } from '../src/logger';
import * as utils from "../src/utils";
// try to load the olm library.
try {
@@ -26,12 +25,3 @@ try {
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available");
}
// also try to set node crypto
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support: some tests will fail');
}
+19
View File
@@ -0,0 +1,19 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import DOMException from "domexception";
global.DOMException = DOMException;
+94
View File
@@ -0,0 +1,94 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MethodKeysOf, mocked, MockedObject } from "jest-mock";
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { User } from "../../src/models/user";
/**
* Mock client with real event emitter
* useful for testing code that listens
* to MatrixClient events
*/
export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
constructor(mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>> = {}) {
super();
Object.assign(this, mockProperties);
}
}
/**
* - make a mock client
* - cast the type to mocked(MatrixClient)
* - spy on MatrixClientPeg.get to return the mock
* eg
* ```
* const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId),
});
* ```
*/
export const getMockClientWithEventEmitter = (
mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>>,
): MockedObject<MatrixClient> => {
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
return mock;
};
/**
* Returns basic mocked client methods related to the current user
* ```
* const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser('@mytestuser:domain'),
});
* ```
*/
export const mockClientMethodsUser = (userId = '@alice:domain') => ({
getUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
});
/**
* Returns basic mocked client methods related to rendering events
* ```
* const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser('@mytestuser:domain'),
});
* ```
*/
export const mockClientMethodsEvents = () => ({
decryptEventIfNeeded: jest.fn(),
getPushActionsForEvent: jest.fn(),
});
/**
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodKeysOf<MatrixClient>, unknown>> => ({
doesServerSupportSeparateAddAndBind: jest.fn(),
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
});
+14 -1
View File
@@ -6,7 +6,7 @@ import '../olm-loader';
import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, MatrixClient, MsgType } from "../../src";
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
@@ -74,6 +74,7 @@ interface IEventOpts {
sender?: string;
skey?: string;
content: IContent;
prev_content?: IContent;
user?: string;
unsigned?: IUnsigned;
redacts?: string;
@@ -103,6 +104,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
prev_content: opts.prev_content,
unsigned: opts.unsigned || {},
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
@@ -371,3 +373,14 @@ export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent>
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",
data: {},
device_display_name: "name",
kind: "http",
lang: "en",
pushkey: "pushpush",
...extra,
});
+76 -8
View File
@@ -17,13 +17,12 @@ limitations under the License.
import MockHttpBackend from "matrix-mock-request";
import { request } from "../../src/matrix";
import { AutoDiscovery } from "../../src/autodiscovery";
describe("AutoDiscovery", function() {
const getHttpBackend = (): MockHttpBackend => {
const httpBackend = new MockHttpBackend();
request(httpBackend.requestFn);
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
return httpBackend;
};
@@ -176,8 +175,7 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (empty string)", function() {
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (empty string)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -205,8 +203,7 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (no property)", function() {
it("should return FAIL_PROMPT when .well-known does not have a base_url for m.homeserver (no property)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {},
@@ -232,8 +229,7 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (disallowed scheme)", function() {
it("should return FAIL_ERROR when .well-known has an invalid base_url for m.homeserver (disallowed scheme)", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
@@ -679,4 +675,76 @@ describe("AutoDiscovery", function() {
}),
]);
});
it("should return FAIL_PROMPT for connection errors", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined);
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT for fetch errors", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").fail(0, new Error("CORS or something"));
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT for invalid JSON", () => {
const httpBackend = getHttpBackend();
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "<html>", true);
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
});
+32 -32
View File
@@ -30,7 +30,7 @@ import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
import { StubStore } from "../../../src/store/stub";
import { IAbortablePromise, MatrixScheduler } from '../../../src';
import { MatrixScheduler } from '../../../src';
const Olm = global.Olm;
@@ -131,7 +131,7 @@ function makeTestClient(cryptoStore) {
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: jest.fn(), // NOP
fetchFn: jest.fn(), // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
@@ -197,7 +197,7 @@ describe("MegolmBackup", function() {
// to tick the clock between the first try and the retry.
const realSetTimeout = global.setTimeout;
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
return realSetTimeout(f, n/100);
return realSetTimeout(f!, n/100);
});
});
@@ -298,25 +298,25 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -381,25 +381,25 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -439,7 +439,7 @@ describe("MegolmBackup", function() {
new Promise<void>((resolve, reject) => {
let backupInfo;
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
@@ -449,23 +449,23 @@ describe("MegolmBackup", function() {
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
data.auth_data, client.getCrossSigningId(), "@alice:bar",
(data as Record<string, any>).auth_data, client.getCrossSigningId(), "@alice:bar",
);
} catch (e) {
reject(e);
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({});
}
backupInfo = data;
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({});
} else if (numCalls === 2) {
expect(method).toBe("GET");
expect(path).toBe("/room_keys/version");
resolve();
return Promise.resolve(backupInfo) as IAbortablePromise<any>;
return Promise.resolve(backupInfo);
} else {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many times"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({});
}
};
}),
@@ -495,7 +495,7 @@ describe("MegolmBackup", function() {
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: jest.fn(), // NOP
fetchFn: jest.fn(), // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
@@ -542,30 +542,30 @@ describe("MegolmBackup", function() {
let numCalls = 0;
await new Promise<void>((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({}) as IAbortablePromise<any>;
return Promise.resolve({} as T);
} else {
return Promise.reject(
new Error("this is an expected failure"),
) as IAbortablePromise<any>;
);
}
};
return client.crypto.backupManager.backupGroupSession(
+1 -1
View File
@@ -141,7 +141,7 @@ describe("Cross Signing", function() {
};
alice.uploadKeySignatures = async () => ({ failures: {} });
alice.setAccountData = async () => ({});
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T> => ({} as T);
alice.getAccountDataFromServer = async <T extends {[k: string]: any}>(): Promise<T | null> => ({} as T);
const authUploadDeviceSigningKeys = async func => await func({});
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
+3 -15
View File
@@ -23,19 +23,10 @@ import { makeTestClients } from './verification/util';
import { encryptAES } from "../../../src/crypto/aes";
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
import { logger } from '../../../src/logger';
import * as utils from "../../../src/utils";
import { ICreateClientOpts } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support');
}
async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial<ICreateClientOpts> = {}) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
@@ -109,16 +100,13 @@ describe("Secrets", function() {
const secretStorage = alice.crypto.secretStorage;
jest.spyOn(alice, 'setAccountData').mockImplementation(
async function(eventType, contents, callback) {
async function(eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
if (callback) {
callback(undefined, undefined);
}
return {};
});
@@ -192,7 +180,7 @@ describe("Secrets", function() {
},
},
);
alice.setAccountData = async function(eventType, contents, callback) {
alice.setAccountData = async function(eventType, contents) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
@@ -332,7 +320,7 @@ describe("Secrets", function() {
);
bob.uploadDeviceSigningKeys = async () => ({});
bob.uploadKeySignatures = jest.fn().mockResolvedValue(undefined);
bob.setAccountData = async function(eventType, contents, callback) {
bob.setAccountData = async function(eventType, contents) {
const event = new MatrixEvent({
type: eventType,
content: contents,
+1 -1
View File
@@ -29,7 +29,7 @@ describe("eventMapperFor", function() {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: function() {} as any, // NOP
fetchFn: function() {} as any, // NOP
store: {
getRoom(roomId: string): Room | null {
return rooms.find(r => r.roomId === roomId);
+62
View File
@@ -0,0 +1,62 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { buildFeatureSupportMap, Feature, ServerSupport } from "../../src/feature";
describe("Feature detection", () => {
it("checks the matrix version", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.3"],
unstable_features: {},
});
expect(support.get(Feature.Thread)).toBe(ServerSupport.Stable);
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
});
it("checks the matrix msc number", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.2"],
unstable_features: {
"org.matrix.msc3771": true,
"org.matrix.msc3773": true,
},
});
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unstable);
});
it("requires two MSCs to pass", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.2"],
unstable_features: {
"org.matrix.msc3771": false,
"org.matrix.msc3773": true,
},
});
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
});
it("requires two MSCs OR matrix versions to pass", async () => {
const support = await buildFeatureSupportMap({
versions: ["v1.4"],
unstable_features: {
"org.matrix.msc3771": false,
"org.matrix.msc3773": true,
},
});
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Stable);
});
});
+39
View File
@@ -1,4 +1,7 @@
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
import { Filter, IFilterDefinition } from "../../src/filter";
import { mkEvent } from "../test-utils/test-utils";
import { EventType } from "../../src";
describe("Filter", function() {
const filterId = "f1lt3ring15g00d4ursoul";
@@ -43,4 +46,40 @@ describe("Filter", function() {
expect(filter.getDefinition()).toEqual(definition);
});
});
describe("setUnreadThreadNotifications", function() {
it("setUnreadThreadNotifications", function() {
filter.setUnreadThreadNotifications(true);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
[UNREAD_THREAD_NOTIFICATIONS.name]: true,
},
},
});
});
});
describe("filterRoomTimeline", () => {
it("should return input if no roomTimelineFilter and roomFilter", () => {
const events = [mkEvent({ type: EventType.Sticker, content: {}, event: true })];
expect(new Filter(undefined).filterRoomTimeline(events)).toStrictEqual(events);
});
it("should filter using components when present", () => {
const definition: IFilterDefinition = {
room: {
timeline: {
types: [EventType.Sticker],
},
},
};
const filter = Filter.fromJson(userId, filterId, definition);
const events = [
mkEvent({ type: EventType.Sticker, content: {}, event: true }),
mkEvent({ type: EventType.RoomMessage, content: {}, event: true }),
];
expect(filter.filterRoomTimeline(events)).toStrictEqual([events[0]]);
});
});
});
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
{
"base": "http://baseUrl",
"params": {
"access_token": "token",
},
"path": "/_matrix/media/r0/upload",
}
`;
+233
View File
@@ -0,0 +1,233 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
describe("FetchHttpApi", () => {
const baseUrl = "http://baseUrl";
const idBaseUrl = "http://idBaseUrl";
const prefix = ClientPrefix.V3;
it("should support aborting multiple times", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.request(Method.Get, "/foo");
api.request(Method.Get, "/baz");
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
api.request(Method.Get, "/bar");
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
api.abort();
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
});
it("should fall back to global fetch if fetchFn not provided", () => {
global.fetch = jest.fn();
expect(global.fetch).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
api.fetch("test");
expect(global.fetch).toHaveBeenCalled();
});
it("should update identity server base url", () => {
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(api.opts.idBaseUrl).toBeUndefined();
api.setIdBaseUrl("https://id.foo.bar");
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
});
describe("idServerRequest", () => {
it("should throw if no idBaseUrl", () => {
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2))
.toThrow("No identity server base URL set");
});
it("should send params as query string for GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
});
it("should send params as body for non-GET requests", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
const params = { foo: "bar", via: ["a", "b"] };
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
});
it("should add Authorization header if token provided", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
});
});
it("should return the Response object if onlyData=false", async () => {
const res = { ok: true };
const fetchFn = jest.fn().mockResolvedValue(res);
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
});
it("should return text if json=false", async () => {
const text = "418 I'm a teapot";
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
await expect(api.requestOtherUrl(Method.Get, "http://url", undefined, {
json: false,
})).resolves.toBe(text);
});
it("should send token via query params if useAuthorizationHeader=false", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
});
it("should send token via headers by default", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.authedRequest(Method.Get, "/path");
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not send a token if not calling `authedRequest`", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
});
api.request(Method.Get, "/path");
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
});
it("should ensure no token is leaked out via query params if sending via headers", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", { access_token: "123" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
});
it("should not override manually specified access token via query params", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: false,
});
api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
});
it("should not override manually specified access token via header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
accessToken: "token",
useAuthorizationHeader: true,
});
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Authorization: "Bearer RealToken" },
});
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
});
it("should not override Accept header", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Get, "/path", undefined, undefined, {
headers: { Accept: "text/html" },
});
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
});
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
const fetchFn = jest.fn().mockResolvedValue({
ok: false,
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(JSON.stringify({
errcode: "M_CONSENT_NOT_GIVEN",
error: "Ye shall ask for consent",
})),
});
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
await Promise.all([
emitPromise(emitter, HttpApiEvent.NoConsent),
expect(api.authedRequest(Method.Get, "/path")).rejects.toThrow("Ye shall ask for consent"),
]);
});
describe("authedRequest", () => {
it("should not include token if unset", () => {
const fetchFn = jest.fn();
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Post, "/account/password");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
});
});
});
+228
View File
@@ -0,0 +1,228 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import DOMException from "domexception";
import { mocked } from "jest-mock";
import { ClientPrefix, MatrixHttpApi, Method, UploadResponse } from "../../../src";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
jest.useFakeTimers();
describe("MatrixHttpApi", () => {
const baseUrl = "http://baseUrl";
const prefix = ClientPrefix.V3;
let xhr: Partial<Writeable<XMLHttpRequest>>;
let upload: Promise<UploadResponse>;
const DONE = 0;
global.DOMException = DOMException;
beforeEach(() => {
xhr = {
upload: {} as XMLHttpRequestUpload,
open: jest.fn(),
send: jest.fn(),
abort: jest.fn(),
setRequestHeader: jest.fn(),
onreadystatechange: undefined,
getResponseHeader: jest.fn(),
};
// We stub out XHR here as it is not available in JSDOM
// @ts-ignore
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
// @ts-ignore
global.XMLHttpRequest.DONE = DONE;
});
afterEach(() => {
upload?.catch(() => {});
// Abort any remaining requests
xhr.readyState = DONE;
xhr.status = 0;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
});
it("should fall back to `fetch` where xhr is unavailable", () => {
global.XMLHttpRequest = undefined;
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
expect(fetchFn).toHaveBeenCalled();
});
it("should prefer xhr where available", () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
upload = api.uploadContent({} as File);
expect(fetchFn).not.toHaveBeenCalled();
expect(xhr.open).toHaveBeenCalled();
});
it("should send access token in query params if header disabled", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
accessToken: "token",
useAuthorizationHeader: false,
});
upload = api.uploadContent({} as File);
expect(xhr.open)
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?access_token=token");
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
});
it("should send access token in header by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
accessToken: "token",
});
upload = api.uploadContent({} as File);
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
});
it("should include filename by default", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File, { name: "name" });
expect(xhr.open)
.toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload?filename=name");
});
it("should allow not sending the filename", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/r0/upload");
});
it("should abort xhr when the upload is aborted", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
api.cancelUpload(upload);
expect(xhr.abort).toHaveBeenCalled();
return expect(upload).rejects.toThrow("Aborted");
});
it("should timeout if no progress in 30s", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
jest.advanceTimersByTime(25000);
// @ts-ignore
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
jest.advanceTimersByTime(25000);
expect(xhr.abort).not.toHaveBeenCalled();
jest.advanceTimersByTime(5000);
expect(xhr.abort).toHaveBeenCalled();
});
it("should call progressHandler", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
const progressHandler = jest.fn();
upload = api.uploadContent({} as File, { progressHandler });
const progressEvent = new Event("progress") as ProgressEvent;
Object.assign(progressEvent, { loaded: 1, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
Object.assign(progressEvent, { loaded: 95, total: 100 });
// @ts-ignore
xhr.upload.onprogress(progressEvent);
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
});
it("should error when no response body", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = "";
xhr.status = 200;
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("No response body.");
});
it("should error on a 400-code", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
xhr.status = 404;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).rejects.toThrow("Not found");
});
it("should return response on successful upload", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
xhr.status = 200;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
});
it("should abort xhr when calling `cancelUpload`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
expect(api.cancelUpload(upload)).toBeTruthy();
expect(xhr.abort).toHaveBeenCalled();
});
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
xhr.readyState = DONE;
xhr.status = 500;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
await upload.catch(() => {});
expect(api.cancelUpload(upload)).toBeFalsy();
expect(xhr.abort).not.toHaveBeenCalled();
});
it("should return active uploads in `getCurrentUploads`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
upload = api.uploadContent({} as File);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeTruthy();
api.cancelUpload(upload);
expect(api.getCurrentUploads().find(u => u.promise === upload)).toBeFalsy();
});
it("should return expected object from `getContentUri`", () => {
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, accessToken: "token" });
expect(api.getContentUri()).toMatchSnapshot();
});
});
+219
View File
@@ -0,0 +1,219 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import {
anySignal,
ConnectionError,
HTTPError,
MatrixError,
parseErrorResponse,
retryNetworkOperation,
timeoutSignal,
} from "../../../src";
import { sleep } from "../../../src/utils";
jest.mock("../../../src/utils");
describe("timeoutSignal", () => {
jest.useFakeTimers();
it("should fire abort signal after specified timeout", () => {
const signal = timeoutSignal(3000);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(3000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
});
describe("anySignal", () => {
jest.useFakeTimers();
it("should fire when any signal fires", () => {
const { signal } = anySignal([
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
jest.advanceTimersByTime(2000);
expect(signal.aborted).toBeTruthy();
expect(onabort).toHaveBeenCalled();
});
it("should cleanup when instructed", () => {
const { signal, cleanup } = anySignal([
timeoutSignal(3000),
timeoutSignal(2000),
]);
const onabort = jest.fn();
signal.onabort = onabort;
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
cleanup();
jest.advanceTimersByTime(2000);
expect(signal.aborted).toBeFalsy();
expect(onabort).not.toHaveBeenCalled();
});
it("should abort immediately if passed an aborted signal", () => {
const controller = new AbortController();
controller.abort();
const { signal } = anySignal([controller.signal]);
expect(signal.aborted).toBeTruthy();
});
});
describe("parseErrorResponse", () => {
it("should resolve Matrix Errors from XHR", () => {
expect(parseErrorResponse({
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
status: 500,
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500));
});
it("should resolve Matrix Errors from fetch", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500));
});
it("should resolve Matrix Errors from XHR with urls", () => {
expect(parseErrorResponse({
responseURL: "https://example.com",
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
status: 500,
} as XMLHttpRequest, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500, "https://example.com"));
});
it("should resolve Matrix Errors from fetch with urls", () => {
expect(parseErrorResponse({
url: "https://example.com",
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new MatrixError({
errcode: "TEST",
}, 500, "https://example.com"));
});
it("should set a sensible default error message on MatrixError", () => {
let err = new MatrixError();
expect(err.message).toEqual("MatrixError: Unknown message");
err = new MatrixError({
error: "Oh no",
});
expect(err.message).toEqual("MatrixError: Oh no");
});
it("should handle no type gracefully", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}')).toStrictEqual(new HTTPError("Server returned 500 error", 500));
});
it("should handle invalid type gracefully", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? " " : null;
},
},
status: 500,
} as Response, '{"errcode": "TEST"}'))
.toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
});
it("should handle plaintext errors", () => {
expect(parseErrorResponse({
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "text/plain" : null;
},
},
status: 418,
} as Response, "I'm a teapot")).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418));
});
});
describe("retryNetworkOperation", () => {
it("should retry given number of times with exponential sleeps", async () => {
const err = new ConnectionError("test");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(4);
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
});
it("should bail out on errors other than ConnectionError", async () => {
const err = new TypeError("invalid JSON");
const fn = jest.fn().mockRejectedValue(err);
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
expect(fn).toHaveBeenCalledTimes(1);
});
it("should return newest ConnectionError when giving up", async () => {
const err1 = new ConnectionError("test1");
const err2 = new ConnectionError("test2");
const err3 = new ConnectionError("test3");
const errors = [err1, err2, err3];
const fn = jest.fn().mockImplementation(() => {
throw errors.shift();
});
mocked(sleep).mockResolvedValue(undefined);
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
});
});
+6 -12
View File
@@ -18,7 +18,7 @@ limitations under the License.
import { MatrixClient } from "../../src/client";
import { logger } from "../../src/logger";
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
import { MatrixError } from "../../src/http-api";
import { HTTPError, MatrixError } from "../../src/http-api";
import { sleep } from "../../src/utils";
import { randomString } from "../../src/randomstring";
@@ -219,8 +219,7 @@ describe("InteractiveAuth", () => {
params: {
[AuthType.Password]: { param: "aa" },
},
});
err.httpStatus = 401;
}, 401);
throw err;
});
@@ -260,7 +259,6 @@ describe("InteractiveAuth", () => {
const requestEmailToken = jest.fn();
const ia = new InteractiveAuth({
authData: null,
matrixClient: getFakeClient(),
stateUpdated,
doRequest,
@@ -282,8 +280,7 @@ describe("InteractiveAuth", () => {
params: {
[AuthType.Password]: { param: "aa" },
},
});
err.httpStatus = 401;
}, 401);
throw err;
});
@@ -338,8 +335,7 @@ describe("InteractiveAuth", () => {
params: {
[AuthType.Password]: { param: "aa" },
},
});
err.httpStatus = 401;
}, 401);
throw err;
});
@@ -374,8 +370,7 @@ describe("InteractiveAuth", () => {
},
error: "Mock Error 1",
errcode: "MOCKERR1",
});
err.httpStatus = 401;
}, 401);
throw err;
});
@@ -402,8 +397,7 @@ describe("InteractiveAuth", () => {
doRequest.mockImplementation((authData) => {
logger.log("request1", authData);
expect(authData).toEqual({ "session": "sessionId" }); // has existing sessionId
const err = new Error('myerror');
(err as any).httpStatus = 401;
const err = new HTTPError('myerror', 401);
throw err;
});
+43
View File
@@ -0,0 +1,43 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { LocalNotificationSettings } from "../../src/@types/local_notifications";
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix";
import { TestClient } from '../TestClient';
let client: MatrixClient;
describe("Local notification settings", () => {
beforeEach(() => {
client = (new TestClient(
"@alice:matrix.org", "123", undefined, undefined, undefined,
)).client;
client.setAccountData = jest.fn();
});
describe("Lets you set local notification settings", () => {
it("stores settings in account data", () => {
const deviceId = "device";
const settings: LocalNotificationSettings = { is_silenced: true };
client.setLocalNotificationSettings(deviceId, settings);
expect(client.setAccountData).toHaveBeenCalledWith(
`${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`,
settings,
);
});
});
});
+14 -23
View File
@@ -103,7 +103,7 @@ describe("MatrixClient", function() {
];
let acceptKeepalives: boolean;
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
function httpReq(method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return Promise.resolve({
unstable_features: {
@@ -132,7 +132,6 @@ describe("MatrixClient", function() {
method: method,
path: path,
};
pendingLookup.promise.abort = () => {}; // to make it a valid IAbortablePromise
return pendingLookup.promise;
}
if (next.path === path && next.method === method) {
@@ -178,7 +177,7 @@ describe("MatrixClient", function() {
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
request: function() {} as any, // NOP
fetchFn: function() {} as any, // NOP
store: store,
scheduler: scheduler,
userId: userId,
@@ -1153,8 +1152,7 @@ describe("MatrixClient", function() {
// event type combined
const expectedEventType = M_BEACON_INFO.name;
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy();
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(method).toBe('PUT');
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
@@ -1168,7 +1166,7 @@ describe("MatrixClient", function() {
await client.unstable_setLiveBeacon(roomId, content);
// event type combined
const [, , path, , requestContent] = client.http.authedRequest.mock.calls[0];
const [, path, , requestContent] = client.http.authedRequest.mock.calls[0];
expect(path).toEqual(
`/rooms/${encodeURIComponent(roomId)}/state/` +
`${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`,
@@ -1229,7 +1227,7 @@ describe("MatrixClient", function() {
it("is called with plain text topic and callback and sends state event", async () => {
const sendStateEvent = createSendStateEventMock("pizza");
client.sendStateEvent = sendStateEvent;
await client.setRoomTopic(roomId, "pizza", () => {});
await client.setRoomTopic(roomId, "pizza");
expect(sendStateEvent).toHaveBeenCalledTimes(1);
});
@@ -1244,15 +1242,9 @@ describe("MatrixClient", function() {
describe("setPassword", () => {
const auth = { session: 'abcdef', type: 'foo' };
const newPassword = 'newpassword';
const callback = () => {};
const passwordTest = (expectedRequestContent: any, expectedCallback?: Function) => {
const [callback, method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
if (expectedCallback) {
expect(callback).toBe(expectedCallback);
} else {
expect(callback).toBeFalsy();
}
const passwordTest = (expectedRequestContent: any) => {
const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0];
expect(method).toBe('POST');
expect(path).toEqual('/account/password');
expect(queryParams).toBeFalsy();
@@ -1269,8 +1261,8 @@ describe("MatrixClient", function() {
});
it("no logout_devices specified + callback", async () => {
await client.setPassword(auth, newPassword, callback);
passwordTest({ auth, new_password: newPassword }, callback);
await client.setPassword(auth, newPassword);
passwordTest({ auth, new_password: newPassword });
});
it("overload logoutDevices=true", async () => {
@@ -1279,8 +1271,8 @@ describe("MatrixClient", function() {
});
it("overload logoutDevices=true + callback", async () => {
await client.setPassword(auth, newPassword, true, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: true }, callback);
await client.setPassword(auth, newPassword, true);
passwordTest({ auth, new_password: newPassword, logout_devices: true });
});
it("overload logoutDevices=false", async () => {
@@ -1289,8 +1281,8 @@ describe("MatrixClient", function() {
});
it("overload logoutDevices=false + callback", async () => {
await client.setPassword(auth, newPassword, false, callback);
passwordTest({ auth, new_password: newPassword, logout_devices: false }, callback);
await client.setPassword(auth, newPassword, false);
passwordTest({ auth, new_password: newPassword, logout_devices: false });
});
});
@@ -1305,8 +1297,7 @@ describe("MatrixClient", function() {
const result = await client.getLocalAliases(roomId);
// Current version of the endpoint we support is v3
const [callback, method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
expect(callback).toBeFalsy();
const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0];
expect(data).toBeFalsy();
expect(method).toBe('GET');
expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`);
+3 -5
View File
@@ -565,7 +565,7 @@ describe("MSC3089TreeSpace", () => {
rooms = {};
rooms[tree.roomId] = parentRoom;
(<any>tree).room = parentRoom; // override readonly
client.getRoom = (r) => rooms[r];
client.getRoom = (r) => rooms[r ?? ""];
clientSendStateFn = jest.fn()
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
@@ -890,9 +890,8 @@ describe("MSC3089TreeSpace", () => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
});
return Promise.resolve(mxc);
return Promise.resolve({ content_uri: mxc });
});
client.uploadContent = uploadFn;
@@ -950,9 +949,8 @@ describe("MSC3089TreeSpace", () => {
expect(contents.length).toEqual(fileContents.length);
expect(opts).toMatchObject({
includeFilename: false,
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
});
return Promise.resolve(mxc);
return Promise.resolve({ content_uri: mxc });
});
client.uploadContent = uploadFn;
+36 -1
View File
@@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "../../../src/models/event";
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils";
import { EventType } from "../../../src";
import { Crypto } from "../../../src/crypto";
describe('MatrixEvent', () => {
it('should create copies of itself', () => {
@@ -84,4 +87,36 @@ describe('MatrixEvent', () => {
expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).toBeUndefined();
});
it("should abort decryption if fails with an error other than a DecryptionError", async () => {
const ev = new MatrixEvent({
type: EventType.RoomMessageEncrypted,
content: {
body: "Test",
},
event_id: "$event1:server",
});
await ev.attemptDecryption({
decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")),
} as unknown as Crypto);
expect(ev.isEncrypted()).toBeTruthy();
expect(ev.isBeingDecrypted()).toBeFalsy();
expect(ev.isDecryptionFailure()).toBeFalsy();
});
describe("applyVisibilityEvent", () => {
it("should emit VisibilityChange if a change was made", async () => {
const ev = new MatrixEvent({
type: "m.room.message",
content: {
body: "Test",
},
event_id: "$event1:server",
});
const prom = emitPromise(ev, MatrixEventEvent.VisibilityChange);
ev.applyVisibilityEvent({ visible: false, eventId: ev.getId(), reason: null });
await prom;
});
});
});
+134
View File
@@ -0,0 +1,134 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Feature, ServerSupport } from "../../src/feature";
import {
EventType,
fixNotificationCountOnDecryption,
MatrixClient,
MatrixEvent,
MsgType,
NotificationCountType,
RelationType,
Room,
RoomEvent,
} from "../../src/matrix";
import { IActionsObject } from "../../src/pushprocessor";
import { ReEmitter } from "../../src/ReEmitter";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
import { mkEvent, mock } from "../test-utils/test-utils";
let mockClient: MatrixClient;
let room: Room;
let event: MatrixEvent;
let threadEvent: MatrixEvent;
const ROOM_ID = "!roomId:example.org";
let THREAD_ID;
function mkPushAction(notify, highlight): IActionsObject {
return {
notify,
tweaks: {
highlight,
},
};
}
describe("fixNotificationCountOnDecryption", () => {
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
getRoom: jest.fn().mockImplementation(() => room),
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
});
mockClient.reEmitter = mock(ReEmitter, 'ReEmitter');
mockClient.canSupport = new Map();
Object.keys(Feature).forEach(feature => {
mockClient.canSupport.set(feature as Feature, ServerSupport.Stable);
});
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "");
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
event = mkEvent({
type: EventType.RoomMessage,
content: {
msgtype: MsgType.Text,
body: "Hello world!",
},
event: true,
}, mockClient);
THREAD_ID = event.getId();
threadEvent = mkEvent({
type: EventType.RoomMessage,
content: {
"m.relates_to": {
rel_type: RelationType.Thread,
event_id: THREAD_ID,
},
"msgtype": MsgType.Text,
"body": "Thread reply",
},
event: true,
});
room.createThread(THREAD_ID, event, [threadEvent], false);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
});
it("changes the room count to highlight on decryption", () => {
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
it("changes the thread count to highlight on decryption", () => {
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
fixNotificationCountOnDecryption(mockClient, threadEvent);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
});
it("emits events", () => {
const cb = jest.fn();
room.on(RoomEvent.UnreadNotifications, cb);
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
expect(cb).toHaveBeenLastCalledWith({ highlight: 0, total: 1 });
room.setUnreadNotificationCount(NotificationCountType.Highlight, 5);
expect(cb).toHaveBeenLastCalledWith({ highlight: 5, total: 1 });
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 5);
expect(cb).toHaveBeenLastCalledWith({ highlight: 5 }, "$123");
});
});
+69
View File
@@ -0,0 +1,69 @@
/*
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 MockHttpBackend from 'matrix-mock-request';
import { MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
import { mkPusher } from '../test-utils/test-utils';
const realSetTimeout = setTimeout;
function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}
let client: MatrixClient;
let httpBackend: MockHttpBackend;
describe("Pushers", () => {
beforeEach(() => {
httpBackend = new MockHttpBackend();
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
});
describe("supports remotely toggling push notifications", () => {
it("migration support when connecting to a legacy homeserver", async () => {
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
unstable_features: {
"org.matrix.msc3881": false,
},
});
httpBackend.when("GET", "/pushers").respond(200, {
pushers: [
mkPusher(),
mkPusher({ [PUSHER_ENABLED.name]: true }),
mkPusher({ [PUSHER_ENABLED.name]: false }),
],
});
const promise = client.getPushers();
await httpBackend.flushAllExpected();
await flushPromises();
const response = await promise;
expect(response.pushers[0][PUSHER_ENABLED.name]).toBe(true);
expect(response.pushers[1][PUSHER_ENABLED.name]).toBe(true);
expect(response.pushers[2][PUSHER_ENABLED.name]).toBe(false);
});
});
});
+100 -2
View File
@@ -1,6 +1,6 @@
import * as utils from "../test-utils/test-utils";
import { PushProcessor } from "../../src/pushprocessor";
import { EventType, MatrixClient, MatrixEvent } from "../../src";
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
describe('NotificationService', function() {
const testUserId = "@ali:matrix.org";
@@ -336,4 +336,102 @@ describe('NotificationService', function() {
enabled: true,
}, testEvent)).toBe(true);
});
describe("performCustomEventHandling()", () => {
const getActionsForEvent = (prevContent: IContent, content: IContent): IActionsObject => {
testEvent = utils.mkEvent({
type: "org.matrix.msc3401.call",
room: testRoomId,
user: "@alice:foo",
skey: "state_key",
event: true,
content: content,
prev_content: prevContent,
});
return pushProcessor.actionsForEvent(testEvent);
};
const assertDoesNotify = (actions: IActionsObject): void => {
expect(actions.notify).toBeTruthy();
expect(actions.tweaks.sound).toBeTruthy();
expect(actions.tweaks.highlight).toBeFalsy();
};
const assertDoesNotNotify = (actions: IActionsObject): void => {
expect(actions.notify).toBeFalsy();
expect(actions.tweaks.sound).toBeFalsy();
expect(actions.tweaks.highlight).toBeFalsy();
};
it.each(
["m.ring", "m.prompt"],
)("should notify when new group call event appears with %s intent", (intent: string) => {
assertDoesNotify(getActionsForEvent({}, {
"m.intent": intent,
"m.type": "m.voice",
"m.name": "Call",
}));
});
it("should notify when a call is un-terminated", () => {
assertDoesNotify(getActionsForEvent({
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
"m.terminated": "All users left",
}, {
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
}));
});
it("should not notify when call is terminated", () => {
assertDoesNotNotify(getActionsForEvent({
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
}, {
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
"m.terminated": "All users left",
}));
});
it("should ignore with m.room intent", () => {
assertDoesNotNotify(getActionsForEvent({}, {
"m.intent": "m.room",
"m.type": "m.voice",
"m.name": "Call",
}));
});
describe("ignoring non-relevant state changes", () => {
it("should ignore intent changes", () => {
assertDoesNotNotify(getActionsForEvent({
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
}, {
"m.intent": "m.ring",
"m.type": "m.video",
"m.name": "Call",
}));
});
it("should ignore name changes", () => {
assertDoesNotNotify(getActionsForEvent({
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
}, {
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "New call",
}));
});
});
});
});
+2 -2
View File
@@ -17,7 +17,7 @@ limitations under the License.
import MockHttpBackend from 'matrix-mock-request';
import { indexedDB as fakeIndexedDB } from 'fake-indexeddb';
import { IHttpOpts, IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src";
import { IndexedDBStore, MatrixEvent, MemoryStore, Room } from "../../src";
import { MatrixClient } from "../../src/client";
import { ToDeviceBatch } from '../../src/models/ToDeviceMessage';
import { logger } from '../../src/logger';
@@ -89,7 +89,7 @@ describe.each([
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
request: httpBackend.requestFn as IHttpOpts["request"],
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store,
});
});
+167
View File
@@ -0,0 +1,167 @@
/*
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 MockHttpBackend from 'matrix-mock-request';
import { ReceiptType } from '../../src/@types/read_receipts';
import { MatrixClient } from "../../src/client";
import { EventType } from '../../src/matrix';
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
import { encodeUri } from '../../src/utils';
import * as utils from "../test-utils/test-utils";
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}
let client: MatrixClient;
let httpBackend: MockHttpBackend;
const THREAD_ID = "$thread_event_id";
const ROOM_ID = "!123:matrix.org";
const threadEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a thread",
"m.relates_to": {
"event_id": THREAD_ID,
"m.in_reply_to": {
"event_id": THREAD_ID,
},
"rel_type": "m.thread",
},
},
});
const roomEvent = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: ROOM_ID,
content: {
"body": "Hello from a room",
},
});
function mockServerSideSupport(client, hasServerSideSupport) {
const doesServerSupportUnstableFeature = client.doesServerSupportUnstableFeature;
client.doesServerSupportUnstableFeature = (unstableFeature) => {
if (unstableFeature === "org.matrix.msc3771") {
return Promise.resolve(hasServerSideSupport);
} else {
return doesServerSupportUnstableFeature(unstableFeature);
}
};
}
describe("Read receipt", () => {
beforeEach(() => {
httpBackend = new MockHttpBackend();
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
client.isGuest = () => false;
});
describe("sendReceipt", () => {
it("sends a thread read receipt", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId(),
}),
).check((request) => {
expect(request.data.thread_id).toEqual(THREAD_ID);
}).respond(200, {});
mockServerSideSupport(client, true);
client.sendReceipt(threadEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends a room read receipt", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: roomEvent.getId(),
}),
).check((request) => {
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
}).respond(200, {});
mockServerSideSupport(client, true);
client.sendReceipt(roomEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends a room read receipt when there's no server support", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId(),
}),
).check((request) => {
expect(request.data.thread_id).toBeUndefined();
}).respond(200, {});
mockServerSideSupport(client, false);
client.sendReceipt(threadEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends a valid room read receipt even when body omitted", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId(),
}),
).check((request) => {
expect(request.data).toEqual({});
}).respond(200, {});
mockServerSideSupport(client, false);
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
await httpBackend.flushAllExpected();
await flushPromises();
});
});
});
+138 -26
View File
@@ -16,7 +16,7 @@ limitations under the License.
import * as utils from "../test-utils/test-utils";
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
import { RoomState } from "../../src";
import { EventType, RoomState } from "../../src";
describe("RoomMember", function() {
const roomId = "!foo:bar";
@@ -142,33 +142,72 @@ describe("RoomMember", function() {
expect(emitCount).toEqual(1);
});
it("should not honor string power levels.",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": "5",
},
it("should not honor string power levels.", function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": "5",
},
event: true,
});
let emitCount = 0;
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(20);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(emitCount).toEqual(1);
},
event: true,
});
let emitCount = 0;
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(20);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(emitCount).toEqual(1);
});
it("should no-op if given a non-state or unrelated event", () => {
const fn = jest.spyOn(member, "emit");
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
member.setPowerLevelEvent(utils.mkEvent({
type: EventType.RoomPowerLevels,
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": "5",
},
},
skey: "invalid",
event: true,
}));
const nonStateEv = utils.mkEvent({
type: EventType.RoomPowerLevels,
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": "5",
},
},
event: true,
});
delete nonStateEv.event.state_key;
member.setPowerLevelEvent(nonStateEv);
member.setPowerLevelEvent(utils.mkEvent({
type: EventType.Sticker,
room: roomId,
user: userA,
content: {},
event: true,
}));
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
});
});
describe("setTypingEvent", function() {
@@ -234,6 +273,79 @@ describe("RoomMember", function() {
});
});
describe("isKicked", () => {
it("should return false if membership is not `leave`", () => {
const member1 = new RoomMember(roomId, userA);
member1.membership = "join";
expect(member1.isKicked()).toBeFalsy();
const member2 = new RoomMember(roomId, userA);
member2.membership = "invite";
expect(member2.isKicked()).toBeFalsy();
const member3 = new RoomMember(roomId, userA);
expect(member3.isKicked()).toBeFalsy();
});
it("should return false if the membership event is unknown", () => {
const member = new RoomMember(roomId, userA);
member.membership = "leave";
expect(member.isKicked()).toBeFalsy();
});
it("should return false if the member left of their own accord", () => {
const member = new RoomMember(roomId, userA);
member.membership = "leave";
member.events.member = utils.mkMembership({
event: true,
sender: userA,
mship: "leave",
skey: userA,
});
expect(member.isKicked()).toBeFalsy();
});
it("should return true if the member's leave was sent by another user", () => {
const member = new RoomMember(roomId, userA);
member.membership = "leave";
member.events.member = utils.mkMembership({
event: true,
sender: userB,
mship: "leave",
skey: userA,
});
expect(member.isKicked()).toBeTruthy();
});
});
describe("getDMInviter", () => {
it("should return userId of the sender of the invite if is_direct=true", () => {
const member = new RoomMember(roomId, userA);
member.membership = "invite";
member.events.member = utils.mkMembership({
event: true,
sender: userB,
mship: "invite",
skey: userA,
});
member.events.member.event.content!.is_direct = true;
expect(member.getDMInviter()).toBe(userB);
});
it("should not return userId of the sender of the invite if is_direct=false", () => {
const member = new RoomMember(roomId, userA);
member.membership = "invite";
member.events.member = utils.mkMembership({
event: true,
sender: userB,
mship: "invite",
skey: userA,
});
member.events.member.event.content!.is_direct = false;
expect(member.getDMInviter()).toBeUndefined();
});
});
describe("setMembershipEvent", function() {
const joinEvent = utils.mkMembership({
event: true,
+81 -65
View File
@@ -303,92 +303,92 @@ describe("RoomState", function() {
state.setStateEvents(events, { timelineWasEmpty: true });
expect(emitCount).toEqual(1);
});
});
describe('beacon events', () => {
it('adds new beacon info events to state and emits', () => {
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
const emitSpy = jest.spyOn(state, 'emit');
describe('beacon events', () => {
it('adds new beacon info events to state and emits', () => {
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([beaconEvent]);
state.setStateEvents([beaconEvent]);
expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});
expect(state.beacons.size).toEqual(1);
const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
expect(beaconInstance).toBeTruthy();
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
});
it('does not add redacted beacon info events to state', () => {
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
redactedBeaconEvent.makeRedacted(redactionEvent);
const emitSpy = jest.spyOn(state, 'emit');
it('does not add redacted beacon info events to state', () => {
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
redactedBeaconEvent.makeRedacted(redactionEvent);
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([redactedBeaconEvent]);
state.setStateEvents([redactedBeaconEvent]);
// no beacon added
expect(state.beacons.size).toEqual(0);
expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
// no new beacon emit
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
});
// no beacon added
expect(state.beacons.size).toEqual(0);
expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
// no new beacon emit
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
});
it('updates existing beacon info events in state', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
it('updates existing beacon info events in state', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
expect(beaconInstance?.isLive).toEqual(true);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
expect(beaconInstance?.isLive).toEqual(true);
state.setStateEvents([updatedBeaconEvent]);
state.setStateEvents([updatedBeaconEvent]);
// same Beacon
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
});
// same Beacon
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
// updated liveness
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
});
it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() });
redactedBeaconEvent.makeRedacted(redactionEvent);
it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() });
redactedBeaconEvent.makeRedacted(redactionEvent);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
expect(beaconInstance?.isLive).toEqual(true);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
expect(beaconInstance?.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]);
state.setStateEvents([redactedBeaconEvent]);
expect(destroySpy).toHaveBeenCalled();
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
});
expect(destroySpy).toHaveBeenCalled();
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
});
it('updates live beacon ids once after setting state events', () => {
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2');
it('updates live beacon ids once after setting state events', () => {
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, '$beacon1');
const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, '$beacon2');
const emitSpy = jest.spyOn(state, 'emit');
const emitSpy = jest.spyOn(state, 'emit');
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
// called once
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
// called once
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
);
// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
);
state.setStateEvents([updatedLiveBeaconEvent]);
state.setStateEvents([updatedLiveBeaconEvent]);
expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
});
expect(state.hasLiveBeacons).toBe(false);
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3);
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
});
});
@@ -1007,4 +1007,20 @@ describe("RoomState", function() {
});
});
});
describe("mayClientSendStateEvent", () => {
it("should return false if the user isn't authenticated", () => {
expect(state.mayClientSendStateEvent("m.room.message", {
isGuest: jest.fn().mockReturnValue(false),
credentials: {},
} as unknown as MatrixClient)).toBeFalsy();
});
it("should return false if the user is a guest", () => {
expect(state.mayClientSendStateEvent("m.room.message", {
isGuest: jest.fn().mockReturnValue(true),
credentials: { userId: userA },
} as unknown as MatrixClient)).toBeFalsy();
});
});
});
+224 -24
View File
@@ -27,18 +27,21 @@ import {
EventType,
JoinRule,
MatrixEvent,
MatrixEventEvent,
PendingEventOrdering,
RelationType,
RoomEvent,
} from "../../src";
import { EventTimeline } from "../../src/models/event-timeline";
import { IWrappedReceipt, Room } from "../../src/models/room";
import { NotificationCountType, Room } from "../../src/models/room";
import { RoomState } from "../../src/models/room-state";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils";
import { ReceiptType } from "../../src/@types/read_receipts";
import { Thread, ThreadEvent } from "../../src/models/thread";
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
import { WrappedReceipt } from "../../src/models/read-receipt";
import { Crypto } from "../../src/crypto";
describe("Room", function() {
const roomId = "!foo:bar";
@@ -336,12 +339,12 @@ describe("Room", function() {
expect(event.getId()).toEqual(localEventId);
expect(event.status).toEqual(EventStatus.SENDING);
expect(emitRoom).toEqual(room);
expect(oldEventId).toBe(null);
expect(oldStatus).toBe(null);
expect(oldEventId).toBeUndefined();
expect(oldStatus).toBeUndefined();
break;
case 1:
expect(event.getId()).toEqual(remoteEventId);
expect(event.status).toBe(null);
expect(event.status).toBeNull();
expect(emitRoom).toEqual(room);
expect(oldEventId).toEqual(localEventId);
expect(oldStatus).toBe(EventStatus.SENDING);
@@ -370,7 +373,7 @@ describe("Room", function() {
delete eventJson["event_id"];
const localEvent = new MatrixEvent(Object.assign({ event_id: "$temp" }, eventJson));
localEvent.status = EventStatus.SENDING;
expect(localEvent.getTxnId()).toBeNull();
expect(localEvent.getTxnId()).toBeUndefined();
expect(room.timeline.length).toEqual(0);
// first add the local echo. This is done before the /send request is even sent.
@@ -385,7 +388,7 @@ describe("Room", function() {
// then /sync returns the remoteEvent, it should de-dupe based on the event ID.
const remoteEvent = new MatrixEvent(Object.assign({ event_id: realEventId }, eventJson));
expect(remoteEvent.getTxnId()).toBeNull();
expect(remoteEvent.getTxnId()).toBeUndefined();
room.addLiveEvents([remoteEvent]);
// the duplicate strategy code should ensure we don't add a 2nd event to the live timeline
expect(room.timeline.length).toEqual(1);
@@ -1430,6 +1433,19 @@ describe("Room", function() {
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
});
});
describe("hasUserReadUpTo", function() {
it("should acknowledge if an event has been read", function() {
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
]));
expect(room.hasUserReadEvent(userB, eventToAck.getId())).toEqual(true);
});
it("return false for an unknown event", function() {
expect(room.hasUserReadEvent(userB, "unknown_event")).toEqual(false);
});
});
});
describe("tags", function() {
@@ -1521,6 +1537,36 @@ describe("Room", function() {
[eventA, eventB, eventC],
);
});
it("should apply redactions eagerly in the pending event list", () => {
const client = (new TestClient("@alice:example.com", "alicedevice")).client;
const room = new Room(roomId, client, userA, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const eventA = utils.mkMessage({
room: roomId,
user: userA,
msg: "remote 1",
event: true,
});
eventA.status = EventStatus.SENDING;
const redactA = utils.mkEvent({
room: roomId,
user: userA,
type: EventType.RoomRedaction,
content: {},
redacts: eventA.getId(),
event: true,
});
redactA.status = EventStatus.SENDING;
room.addPendingEvent(eventA, "TXN1");
expect(room.getPendingEvents()).toEqual([eventA]);
room.addPendingEvent(redactA, "TXN2");
expect(room.getPendingEvents()).toEqual([eventA, redactA]);
expect(eventA.isRedacted()).toBeTruthy();
});
});
describe("updatePendingEvent", function() {
@@ -1707,7 +1753,7 @@ describe("Room", function() {
});
room.updateMyMembership(JoinRule.Invite);
expect(room.getMyMembership()).toEqual(JoinRule.Invite);
expect(events[0]).toEqual({ membership: "invite", oldMembership: null });
expect(events[0]).toEqual({ membership: "invite", oldMembership: undefined });
events.splice(0); //clear
room.updateMyMembership(JoinRule.Invite);
expect(events.length).toEqual(0);
@@ -2394,7 +2440,7 @@ describe("Room", function() {
});
it("should aggregate relations in thread event timeline set", () => {
Thread.setServerSideSupport(true, true);
Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage();
const rootReaction = mkReaction(threadRoot);
const threadResponse = mkThreadResponse(threadRoot);
@@ -2439,8 +2485,8 @@ describe("Room", function() {
const room = new Room(roomId, client, userA);
it("handles missing receipt type", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null;
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as WrappedReceipt : null;
};
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
@@ -2448,12 +2494,12 @@ describe("Room", function() {
describe("prefers newer receipt", () => {
it("should compare correctly using timelines", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as IWrappedReceipt;
return { eventId: "eventId1" } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as IWrappedReceipt;
return { eventId: "eventId2" } as WrappedReceipt;
}
return null;
};
@@ -2473,12 +2519,12 @@ describe("Room", function() {
room.getUnfilteredTimelineSet = () => ({
compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as IWrappedReceipt;
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as IWrappedReceipt;
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as WrappedReceipt;
}
return null;
};
@@ -2491,9 +2537,9 @@ describe("Room", function() {
room.getUnfilteredTimelineSet = () => ({
compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: 1 } } as IWrappedReceipt;
return { eventId: "eventId2", data: { ts: 1 } } as WrappedReceipt;
}
return null;
};
@@ -2510,12 +2556,12 @@ describe("Room", function() {
});
it("should give precedence to m.read.private", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as IWrappedReceipt;
return { eventId: "eventId1" } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as IWrappedReceipt;
return { eventId: "eventId2" } as WrappedReceipt;
}
return null;
};
@@ -2524,9 +2570,9 @@ describe("Room", function() {
});
it("should give precedence to m.read", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as IWrappedReceipt;
return { eventId: "eventId3" } as WrappedReceipt;
}
return null;
};
@@ -2548,4 +2594,158 @@ describe("Room", function() {
expect(client.roomNameGenerator).toHaveBeenCalled();
});
});
describe("thread notifications", () => {
let room;
beforeEach(() => {
const client = new TestClient(userA).client;
room = new Room(roomId, client, userA);
});
it("defaults to undefined", () => {
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(0);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0);
});
it("lets you set values", () => {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0);
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 10);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(10);
});
it("lets you reset threads notifications", () => {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
room.resetThreadUnreadNotificationCount();
expect(room.threadsAggregateNotificationType).toBe(null);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(0);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(0);
});
it("sets the room threads notification type", () => {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 333);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
});
});
describe("hasThreadUnreadNotification", () => {
it('has no notifications by default', () => {
expect(room.hasThreadUnreadNotification()).toBe(false);
});
it('main timeline notification does not affect this', () => {
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
expect(room.hasThreadUnreadNotification()).toBe(false);
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
expect(room.hasThreadUnreadNotification()).toBe(false);
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
expect(room.hasThreadUnreadNotification()).toBe(true);
});
it('lets you reset', () => {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 1);
expect(room.hasThreadUnreadNotification()).toBe(true);
room.resetThreadUnreadNotificationCount();
expect(room.hasThreadUnreadNotification()).toBe(false);
});
});
describe("threadsAggregateNotificationType", () => {
it("defaults to null", () => {
expect(room.threadsAggregateNotificationType).toBeNull();
});
it("counts multiple threads", () => {
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
room.setThreadUnreadNotificationCount("$456", NotificationCountType.Total, 1);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 0);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Total);
});
it("allows reset", () => {
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
room.setThreadUnreadNotificationCount("$456", NotificationCountType.Total, 1);
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
expect(room.threadsAggregateNotificationType).toBe(NotificationCountType.Highlight);
room.resetThreadUnreadNotificationCount();
expect(room.threadsAggregateNotificationType).toBeNull();
});
});
it("should load pending events from from the store and decrypt if needed", async () => {
const client = new TestClient(userA).client;
client.crypto = {
decryptEvent: jest.fn().mockResolvedValue({ clearEvent: { body: "enc" } }),
} as unknown as Crypto;
client.store.getPendingEvents = jest.fn(async roomId => [{
event_id: "$1:server",
type: "m.room.message",
content: { body: "1" },
sender: "@1:server",
room_id: roomId,
origin_server_ts: 1,
txn_id: "txn1",
}, {
event_id: "$2:server",
type: "m.room.encrypted",
content: { body: "2" },
sender: "@2:server",
room_id: roomId,
origin_server_ts: 2,
txn_id: "txn2",
}]);
const room = new Room(roomId, client, userA, {
pendingEventOrdering: PendingEventOrdering.Detached,
});
await emitPromise(room, RoomEvent.LocalEchoUpdated);
await emitPromise(client, MatrixEventEvent.Decrypted);
await emitPromise(room, RoomEvent.LocalEchoUpdated);
const pendingEvents = room.getPendingEvents();
expect(pendingEvents).toHaveLength(2);
expect(pendingEvents[1].isDecryptionFailure()).toBeFalsy();
expect(pendingEvents[1].isBeingDecrypted()).toBeFalsy();
expect(pendingEvents[1].isEncrypted()).toBeTruthy();
for (const ev of pendingEvents) {
expect(room.getPendingEvent(ev.getId())).toBe(ev);
}
});
describe("getBlacklistUnverifiedDevices", () => {
it("defaults to null", () => {
expect(room.getBlacklistUnverifiedDevices()).toBeNull();
});
it("is updated by setBlacklistUnverifiedDevices", () => {
room.setBlacklistUnverifiedDevices(false);
expect(room.getBlacklistUnverifiedDevices()).toBe(false);
});
});
});
+55 -1
View File
@@ -20,6 +20,7 @@ import 'jest-localstorage-mock';
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
import { defer } from "../../../src/utils";
describe("IndexedDBStore", () => {
afterEach(() => {
@@ -59,7 +60,7 @@ describe("IndexedDBStore", () => {
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(1);
// Simulate a broken IDB
(store.backend as LocalIndexedDBStoreBackend)["db"].transaction = (): IDBTransaction => {
(store.backend as LocalIndexedDBStoreBackend)["db"]!.transaction = (): IDBTransaction => {
const err = new Error("Failed to execute 'transaction' on 'IDBDatabase': " +
"The database connection is closing.");
err.name = "InvalidStateError";
@@ -111,4 +112,57 @@ describe("IndexedDBStore", () => {
await store.setPendingEvents(roomId, []);
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull();
});
it("should resolve isNewlyCreated to true if no database existed initially", async () => {
const store = new IndexedDBStore({
indexedDB,
dbName: "db1",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeTruthy();
});
it("should resolve isNewlyCreated to false if database existed already", async () => {
let store = new IndexedDBStore({
indexedDB,
dbName: "db2",
localStorage,
});
await store.startup();
store = new IndexedDBStore({
indexedDB,
dbName: "db2",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
});
it("should resolve isNewlyCreated to false if database existed already but needs upgrade", async () => {
const deferred = defer<Event>();
// seed db3 to Version 1 so it forces a migration
const req = indexedDB.open("matrix-js-sdk:db3", 1);
req.onupgradeneeded = () => {
const db = req.result;
db.createObjectStore("users", { keyPath: ["userId"] });
db.createObjectStore("accountData", { keyPath: ["type"] });
db.createObjectStore("sync", { keyPath: ["clobber"] });
};
req.onsuccess = deferred.resolve;
await deferred.promise;
req.result.close();
const store = new IndexedDBStore({
indexedDB,
dbName: "db3",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
});
});
+13
View File
@@ -30,6 +30,12 @@ const RES_WITH_AGE = {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
unread_thread_notifications: {
"$143273582443PhrSn:example.org": {
highlight_count: 0,
notification_count: 1,
},
},
timeline: {
events: [
Object.freeze({
@@ -439,6 +445,13 @@ describe("SyncAccumulator", function() {
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
);
});
it("should retrieve unread thread notifications", () => {
sa.accumulate(RES_WITH_AGE);
const output = sa.getJSON();
expect(output.roomsData.join["!foo:bar"]
.unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined();
});
});
});
+19 -4
View File
@@ -26,9 +26,7 @@ describe("utils", function() {
foo: "bar",
baz: "beer@",
};
expect(utils.encodeParams(params)).toEqual(
"foo=bar&baz=beer%40",
);
expect(utils.encodeParams(params).toString()).toEqual("foo=bar&baz=beer%40");
});
it("should handle boolean and numeric values", function() {
@@ -37,7 +35,24 @@ describe("utils", function() {
number: 12345,
boolean: false,
};
expect(utils.encodeParams(params)).toEqual("string=foobar&number=12345&boolean=false");
expect(utils.encodeParams(params).toString()).toEqual("string=foobar&number=12345&boolean=false");
});
it("should handle string arrays", () => {
const params = {
via: ["one", "two", "three"],
};
expect(utils.encodeParams(params).toString()).toEqual("via=one&via=two&via=three");
});
});
describe("decodeParams", () => {
it("should be able to decode multiple values into an array", () => {
const params = "foo=bar&via=a&via=b&via=c";
expect(utils.decodeParams(params)).toEqual({
foo: "bar",
via: ["a", "b", "c"],
});
});
});
+5 -1
View File
@@ -156,9 +156,13 @@ export interface IPusher {
lang: string;
profile_tag?: string;
pushkey: string;
enabled?: boolean | null | undefined;
"org.matrix.msc3881.enabled"?: boolean | null | undefined;
device_id?: string | null;
"org.matrix.msc3881.device_id"?: string | null;
}
export interface IPusherRequest extends IPusher {
export interface IPusherRequest extends Omit<IPusher, "device_id" | "org.matrix.msc3881.device_id"> {
append?: boolean;
}
+28 -3
View File
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "../NamespacedValue";
// disable lint because these are wire responses
/* eslint-disable camelcase */
@@ -29,7 +31,7 @@ export interface IRefreshTokenResponse {
/* eslint-enable camelcase */
/**
* Response to GET login flows as per https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3login
* Response to GET login flows as per https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3login
*/
export interface ILoginFlowsResponse {
flows: LoginFlow[];
@@ -45,13 +47,20 @@ export interface IPasswordFlow extends ILoginFlow {
type: "m.login.password";
}
export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
"delegated_oidc_compatibility",
"org.matrix.msc3824.delegated_oidc_compatibility",
);
/**
* Representation of SSO flow as per https://spec.matrix.org/latest/client-server-api/#client-login-via-sso
* Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
*/
export interface ISSOFlow extends ILoginFlow {
type: "m.login.sso" | "m.login.cas";
// eslint-disable-next-line camelcase
identity_providers?: IIdentityProvider[];
[DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
[DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
}
export enum IdentityProviderBrand {
@@ -71,7 +80,7 @@ export interface IIdentityProvider {
}
/**
* Parameters to login request as per https://spec.matrix.org/latest/client-server-api/#login
* Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login
*/
/* eslint-disable camelcase */
export interface ILoginParams {
@@ -90,3 +99,19 @@ export enum SSOAction {
/** The user intends to register for a new account */
REGISTER = "register",
}
/**
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
* `m.login.token` issuance request.
* Note that this is UNSTABLE and subject to breaking changes without notice.
*/
export interface LoginTokenPostResponse {
/**
* The token to use with `m.login.token` to authenticate.
*/
login_token: string;
/**
* Expiration in seconds.
*/
expires_in: number;
}
+27 -4
View File
@@ -33,10 +33,6 @@ export enum EventType {
RoomGuestAccess = "m.room.guest_access",
RoomServerAcl = "m.room.server_acl",
RoomTombstone = "m.room.tombstone",
/**
* @deprecated Should not be used.
*/
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
SpaceChild = "m.space.child",
SpaceParent = "m.space.parent",
@@ -191,6 +187,33 @@ export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
"m.visibility",
"org.matrix.msc3531.visibility");
/**
* https://github.com/matrix-org/matrix-doc/pull/3881
*
* @experimental
*/
export const PUSHER_ENABLED = new UnstableValue(
"enabled",
"org.matrix.msc3881.enabled");
/**
* https://github.com/matrix-org/matrix-doc/pull/3881
*
* @experimental
*/
export const PUSHER_DEVICE_ID = new UnstableValue(
"device_id",
"org.matrix.msc3881.device_id");
/**
* https://github.com/matrix-org/matrix-doc/pull/3890
*
* @experimental
*/
export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
"m.local_notification_settings",
"org.matrix.msc3890.local_notification_settings");
export interface IEncryptedFile {
url: string;
mimetype?: string;
+2
View File
@@ -30,6 +30,8 @@ declare global {
namespace NodeJS {
interface Global {
localStorage: Storage;
// marker variable used to detect both the browser & node entrypoints being used at once
__js_sdk_entrypoint: unknown;
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
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.
*/
export interface LocalNotificationSettings {
is_silenced: boolean;
}
-5
View File
@@ -40,11 +40,6 @@ export enum Preset {
export type ResizeMethod = "crop" | "scale";
// TODO move to http-api after TSification
export interface IAbortablePromise<T> extends Promise<T> {
abort(): void;
}
export type IdServerUnbindResult = "no-support" | "success";
// Knock and private are reserved keywords which are not yet implemented.
+14 -14
View File
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Callback } from "../client";
import { IContent, IEvent } from "../models/event";
import { Preset, Visibility } from "./partials";
import { IEventWithRoomId, SearchKey } from "./search";
@@ -22,7 +21,7 @@ import { IRoomEventFilter } from "../filter";
import { Direction } from "../models/event-timeline";
import { PushRuleAction } from "./PushRules";
import { IRoomEvent } from "../sync-accumulator";
import { RoomType } from "./event";
import { EventType, RoomType } from "./event";
// allow camelcase as these are things that go onto the wire
/* eslint-disable camelcase */
@@ -98,7 +97,18 @@ export interface ICreateRoomOpts {
name?: string;
topic?: string;
preset?: Preset;
power_level_content_override?: object;
power_level_content_override?: {
ban?: number;
events?: Record<EventType | string, number>;
events_default?: number;
invite?: number;
kick?: number;
notifications?: Record<string, number>;
redact?: number;
state_default?: number;
users?: Record<string, number>;
users_default?: number;
};
creation_content?: object;
initial_state?: ICreateRoomStateEvent[];
invite?: string[];
@@ -119,16 +129,6 @@ export interface IRoomDirectoryOptions {
third_party_instance_id?: string;
}
export interface IUploadOpts {
name?: string;
includeFilename?: boolean;
type?: string;
rawResponse?: boolean;
onlyContentUri?: boolean;
callback?: Callback;
progressHandler?: (state: {loaded: number, total: number}) => void;
}
export interface IAddThreePidOnlyBody {
auth?: {
type: string;
@@ -149,7 +149,7 @@ export interface IRelationsRequestOpts {
from?: string;
to?: string;
limit?: number;
direction?: Direction;
dir?: Direction;
}
export interface IRelationsResponse {
+26
View File
@@ -0,0 +1,26 @@
/*
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 { ServerControlledNamespacedValue } from "../NamespacedValue";
/**
* https://github.com/matrix-org/matrix-doc/pull/3773
*
* @experimental
*/
export const UNREAD_THREAD_NOTIFICATIONS = new ServerControlledNamespacedValue(
"unread_thread_notifications",
"org.matrix.msc3773.unread_thread_notifications");
+29
View File
@@ -0,0 +1,29 @@
/*
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 { IAuthData } from "../interactive-auth";
/**
* Helper type to represent HTTP request body for a UIA enabled endpoint
*/
export type UIARequest<T> = T & {
auth?: IAuthData;
};
/**
* Helper type to represent HTTP response body for a UIA enabled endpoint
*/
export type UIAResponse<T> = T | IAuthData;
+3 -3
View File
@@ -28,7 +28,7 @@ const MAX_BATCH_SIZE = 20;
export class ToDeviceMessageQueue {
private sending = false;
private running = true;
private retryTimeout: number = null;
private retryTimeout: ReturnType<typeof setTimeout> | null = null;
private retryAttempts = 0;
constructor(private client: MatrixClient) {
@@ -68,7 +68,7 @@ export class ToDeviceMessageQueue {
logger.debug("Attempting to send queued to-device messages");
this.sending = true;
let headBatch;
let headBatch: IndexedToDeviceBatch;
try {
while (this.running) {
headBatch = await this.client.store.getOldestToDeviceBatch();
@@ -92,7 +92,7 @@ export class ToDeviceMessageQueue {
// bored and giving up for now
if (Math.floor(e.httpStatus / 100) === 4) {
logger.error("Fatal error when sending to-device message - dropping to-device batch!", e);
await this.client.store.removeToDeviceBatch(headBatch.id);
await this.client.store.removeToDeviceBatch(headBatch!.id);
} else {
logger.info("Automatic retry limit reached for to-device messages.");
}
+63 -41
View File
@@ -17,10 +17,9 @@ limitations under the License.
/** @module auto-discovery */
import { ServerResponse } from "http";
import { IClientWellKnown, IWellKnownConfig } from "./client";
import { logger } from './logger';
import { MatrixError, Method, timeoutSignal } from "./http-api";
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
@@ -395,6 +394,19 @@ export class AutoDiscovery {
}
}
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
if (this.fetchFn) {
return this.fetchFn(resource, options);
}
return global.fetch(resource, options);
}
private static fetchFn?: typeof global.fetch;
public static setFetchFn(fetchFn: typeof global.fetch): void {
AutoDiscovery.fetchFn = fetchFn;
}
/**
* Fetches a JSON object from a given URL, as expected by all .well-known
* related lookups. If the server gives a 404 then the `action` will be
@@ -411,45 +423,55 @@ export class AutoDiscovery {
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
private static fetchWellKnownObject(uri: string): Promise<IWellKnownConfig> {
return new Promise((resolve) => {
// eslint-disable-next-line
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
{ method: "GET", uri, timeout: 5000 },
(error: Error, response: ServerResponse, body: string) => {
if (error || response?.statusCode < 200 || response?.statusCode >= 300) {
const result = { error, raw: {} };
return resolve(response?.statusCode === 404
? {
...result,
action: AutoDiscoveryAction.IGNORE,
reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN,
} : {
...result,
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: error?.message || "General failure",
});
}
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
let response: Response;
try {
return resolve({
raw: JSON.parse(body),
action: AutoDiscoveryAction.SUCCESS,
});
} catch (err) {
return resolve({
error: err,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: err?.name === "SyntaxError"
? AutoDiscovery.ERROR_INVALID_JSON
: AutoDiscovery.ERROR_INVALID,
});
}
},
);
});
try {
response = await AutoDiscovery.fetch(url, {
method: Method.Get,
signal: timeoutSignal(5000),
});
if (response.status === 404) {
return {
raw: {},
action: AutoDiscoveryAction.IGNORE,
reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN,
};
}
if (!response.ok) {
return {
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: "General failure",
};
}
} catch (err) {
const error = err as Error | string | undefined;
return {
error,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: (<Error>error)?.message || "General failure",
};
}
try {
return {
raw: await response.json(),
action: AutoDiscoveryAction.SUCCESS,
};
} catch (err) {
const error = err as Error | string | undefined;
return {
error,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: (error as MatrixError)?.name === "SyntaxError"
? AutoDiscovery.ERROR_INVALID_JSON
: AutoDiscovery.ERROR_INVALID,
};
}
}
}
+2 -15
View File
@@ -14,25 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import request from "browser-request";
import queryString from "qs";
import * as matrixcs from "./matrix";
if (matrixcs.getRequest()) {
if (global.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
// we mimic `request`'s query string interface to make it all work
// as expected.
// browser-request will happily take the constructed string as the
// query string without trying to modify it further.
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
return request(opts, fn);
});
global.__js_sdk_entrypoint = true;
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
+728 -787
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -35,7 +35,7 @@ import * as utils from "./utils";
*/
export function getHttpUriForMxc(
baseUrl: string,
mxc: string,
mxc?: string,
width?: number,
height?: number,
resizeMethod?: string,
+9 -3
View File
@@ -31,7 +31,7 @@ import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from "../matrix";
import { ISignatures } from "../@types/signed";
import { CryptoStore } from "./store/base";
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
import { ISecretStorageKeyInfo } from "./api";
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
@@ -699,7 +699,10 @@ export class DeviceTrustLevel {
export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks {
return {
getCrossSigningKeyCache: async function(type: string, _expectedPublicKey: string): Promise<Uint8Array> {
getCrossSigningKeyCache: async function(
type: keyof SecretStorePrivateKeys,
_expectedPublicKey: string,
): Promise<Uint8Array> {
const key = await new Promise<any>((resolve) => {
return store.doTxn(
'readonly',
@@ -718,7 +721,10 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
return key;
}
},
storeCrossSigningKeyCache: async function(type: string, key: Uint8Array): Promise<void> {
storeCrossSigningKeyCache: async function(
type: keyof SecretStorePrivateKeys,
key: Uint8Array,
): Promise<void> {
if (!(key instanceof Uint8Array)) {
throw new Error(
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
+5 -5
View File
@@ -18,7 +18,7 @@ import { logger } from "../logger";
import { MatrixEvent } from "../models/event";
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { Method, PREFIX_UNSTABLE } from "../http-api";
import { Method, ClientPrefix } from "../http-api";
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
import {
ClientEvent,
@@ -241,19 +241,19 @@ export class EncryptionSetupOperation {
// Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing.
await baseApis.http.authedRequest(
undefined, Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
Method.Put, "/room_keys/version/" + this.keyBackupInfo.version,
undefined, {
algorithm: this.keyBackupInfo.algorithm,
auth_data: this.keyBackupInfo.auth_data,
},
{ prefix: PREFIX_UNSTABLE },
{ prefix: ClientPrefix.V3 },
);
} else {
// add new key backup
await baseApis.http.authedRequest(
undefined, Method.Post, "/room_keys/version",
Method.Post, "/room_keys/version",
undefined, this.keyBackupInfo,
{ prefix: PREFIX_UNSTABLE },
{ prefix: ClientPrefix.V3 },
);
}
}
+4 -4
View File
@@ -308,10 +308,10 @@ export class OlmDevice {
* @private
*/
private getAccount(txn: unknown, func: (account: Account) => void): void {
this.cryptoStore.getAccount(txn, (pickledAccount: string) => {
this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
try {
account.unpickle(this.pickleKey, pickledAccount);
account.unpickle(this.pickleKey, pickledAccount!);
func(account);
} finally {
account.free();
@@ -350,8 +350,8 @@ export class OlmDevice {
IndexedDBCryptoStore.STORE_SESSIONS,
],
(txn) => {
this.cryptoStore.getAccount(txn, (pickledAccount: string) => {
result.pickledAccount = pickledAccount;
this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
result.pickledAccount = pickledAccount!;
});
result.sessions = [];
// Note that the pickledSession object we get in the callback
+1 -1
View File
@@ -549,7 +549,7 @@ export class SecretStorage {
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
content.sender_key,
event.getSenderKey() || "",
);
if (senderKeyUser !== event.getSender()) {
logger.error("sending device does not belong to the user it claims to be from");
+18 -116
View File
@@ -14,129 +14,39 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { BinaryLike } from "crypto";
import { getCrypto } from '../utils';
import { decodeBase64, encodeBase64 } from './olmlib';
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
import { subtleCrypto, crypto, TextEncoder } from "./crypto";
// salt for HKDF, with 8 bytes of zeros
const zeroSalt = new Uint8Array(8);
export interface IEncryptedPayload {
[key: string]: any; // extensible
iv?: string;
ciphertext?: string;
mac?: string;
iv: string;
ciphertext: string;
mac: string;
}
/**
* encrypt a string in Node.js
* encrypt a string
*
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptNode(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
let iv;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = crypto.randomBytes(16);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = deriveKeysNode(key, name);
const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv);
const ciphertext = Buffer.concat([
cipher.update(data, "utf8"),
cipher.final(),
]);
const hmac = crypto.createHmac("sha256", hmacKey)
.update(ciphertext).digest("base64");
return {
iv: encodeBase64(iv),
ciphertext: ciphertext.toString("base64"),
mac: hmac,
};
}
/**
* decrypt a string in Node.js
*
* @param {object} data the encrypted data
* @param {string} data.ciphertext the ciphertext in base64
* @param {string} data.iv the initialization vector in base64
* @param {string} data.mac the HMAC in base64
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptNode(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
const [aesKey, hmacKey] = deriveKeysNode(key, name);
const hmac = crypto.createHmac("sha256", hmacKey)
.update(Buffer.from(data.ciphertext, "base64"))
.digest("base64").replace(/=+$/g, '');
if (hmac !== data.mac.replace(/=+$/g, '')) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const decipher = crypto.createDecipheriv(
"aes-256-ctr", aesKey, decodeBase64(data.iv),
);
return decipher.update(data.ciphertext, "base64", "utf8")
+ decipher.final("utf8");
}
function deriveKeysNode(key: BinaryLike, name: string): [Buffer, Buffer] {
const crypto = getCrypto();
const prk = crypto.createHmac("sha256", zeroSalt).update(key).digest();
const b = Buffer.alloc(1, 1);
const aesKey = crypto.createHmac("sha256", prk)
.update(name, "utf8").update(b).digest();
b[0] = 2;
const hmacKey = crypto.createHmac("sha256", prk)
.update(aesKey).update(name, "utf8").update(b).digest();
return [aesKey, hmacKey];
}
/**
* encrypt a string in Node.js
*
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
let iv;
export async function encryptAES(
data: string,
key: Uint8Array,
name: string,
ivStr?: string,
): Promise<IEncryptedPayload> {
let iv: Uint8Array;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);
crypto.getRandomValues(iv);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
@@ -144,7 +54,7 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr
iv[8] &= 0x7f;
}
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
const [aesKey, hmacKey] = await deriveKeys(key, name);
const encodedData = new TextEncoder().encode(data);
const ciphertext = await subtleCrypto.encrypt(
@@ -171,7 +81,7 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr
}
/**
* decrypt a string in the browser
* decrypt a string
*
* @param {object} data the encrypted data
* @param {string} data.ciphertext the ciphertext in base64
@@ -180,8 +90,8 @@ async function encryptBrowser(data: string, key: Uint8Array, name: string, ivStr
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptBrowser(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
const [aesKey, hmacKey] = await deriveKeys(key, name);
const ciphertext = decodeBase64(data.ciphertext);
@@ -207,7 +117,7 @@ async function decryptBrowser(data: IEncryptedPayload, key: Uint8Array, name: st
return new TextDecoder().decode(new Uint8Array(plaintext));
}
async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
const hkdfkey = await subtleCrypto.importKey(
'raw',
key,
@@ -253,14 +163,6 @@ async function deriveKeysBrowser(key: Uint8Array, name: string): Promise<[Crypto
return Promise.all([aesProm, hmacProm]);
}
export function encryptAES(data: string, key: Uint8Array, name: string, ivStr?: string): Promise<IEncryptedPayload> {
return subtleCrypto ? encryptBrowser(data, key, name, ivStr) : encryptNode(data, key, name, ivStr);
}
export function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise<string> {
return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name);
}
// string of zeroes, for calculating the key check
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
+1 -1
View File
@@ -23,7 +23,7 @@ limitations under the License.
import { MatrixClient } from "../../client";
import { Room } from "../../models/room";
import { OlmDevice } from "../OlmDevice";
import { MatrixEvent, RoomMember } from "../..";
import { MatrixEvent, RoomMember } from "../../matrix";
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
import { DeviceInfo } from "../deviceinfo";
import { IRoomEncryption } from "../RoomList";
+16 -9
View File
@@ -1134,8 +1134,9 @@ class MegolmEncryption extends EncryptionAlgorithm {
// The global value is treated as a default for when rooms don't specify a value.
let isBlacklisting = this.crypto.getGlobalBlacklistUnverifiedDevices();
if (typeof room.getBlacklistUnverifiedDevices() === 'boolean') {
isBlacklisting = room.getBlacklistUnverifiedDevices();
const isRoomBlacklisting = room.getBlacklistUnverifiedDevices();
if (typeof isRoomBlacklisting === 'boolean') {
isBlacklisting = isRoomBlacklisting;
}
// We are happy to use a cached version here: we assume that if we already
@@ -1444,9 +1445,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
memberEvent?.getPrevContent()?.membership === "invite");
const fromUs = event.getSender() === this.baseApis.getUserId();
if (!weRequested) {
// If someone sends us an unsolicited key and it's not
// shared history, ignore it
if (!weRequested && !fromUs) {
// If someone sends us an unsolicited key and they're
// not one of our other devices and it's not shared
// history, ignore it
if (!extraSessionData.sharedHistory) {
logger.log("forwarded key not shared history - ignoring");
return;
@@ -1455,7 +1457,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
// If someone sends us an unsolicited key for a room
// we're already in, and they're not one of our other
// devices or the one who invited us, ignore it
if (room && !fromInviter && !fromUs) {
if (room && !fromInviter) {
logger.log("forwarded key not from inviter or from us - ignoring");
return;
}
@@ -1795,12 +1797,17 @@ class MegolmDecryption extends DecryptionAlgorithm {
* @private
* @param {String} senderKey
* @param {String} sessionId
* @param {Boolean} keyTrusted
* @param {Boolean} forceRedecryptIfUntrusted whether messages that were already
* successfully decrypted using untrusted keys should be re-decrypted
*
* @return {Boolean} whether all messages were successfully
* decrypted with trusted keys
*/
private async retryDecryption(senderKey: string, sessionId: string, keyTrusted?: boolean): Promise<boolean> {
private async retryDecryption(
senderKey: string,
sessionId: string,
forceRedecryptIfUntrusted?: boolean,
): Promise<boolean> {
const senderPendingEvents = this.pendingEvents.get(senderKey);
if (!senderPendingEvents) {
return true;
@@ -1815,7 +1822,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
await Promise.all([...pending].map(async (ev) => {
try {
await ev.attemptDecryption(this.crypto, { isRetry: true, keyTrusted });
await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
} catch (e) {
// don't die if something goes wrong
}
+1 -1
View File
@@ -30,7 +30,7 @@ import {
registerAlgorithm,
} from "./base";
import { Room } from '../../models/room';
import { MatrixEvent } from "../..";
import { MatrixEvent } from "../../models/event";
import { IEventDecryptionResult } from "../index";
import { IInboundSession } from "../OlmDevice";
+20 -18
View File
@@ -26,13 +26,20 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
import { DeviceInfo } from "./deviceinfo";
import { DeviceTrustLevel } from './CrossSigning';
import { keyFromPassphrase } from './key_passphrase';
import { getCrypto, sleep } from "../utils";
import { sleep } from "../utils";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { encodeRecoveryKey } from './recoverykey';
import { calculateKeyCheck, decryptAES, encryptAES } from './aes';
import { IAes256AuthData, ICurve25519AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
import {
Curve25519SessionData,
IAes256AuthData,
ICurve25519AuthData,
IKeyBackupInfo,
IKeyBackupSession,
} from "./keybackup";
import { UnstableValue } from "../NamespacedValue";
import { CryptoEvent, IMegolmSessionData } from "./index";
import { crypto } from "./crypto";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms
@@ -677,7 +684,9 @@ export class Curve25519 implements BackupAlgorithm {
return this.publicKey.encrypt(JSON.stringify(plainText));
}
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
public async decryptSessions(
sessions: Record<string, IKeyBackupSession<Curve25519SessionData>>,
): Promise<IMegolmSessionData[]> {
const privKey = await this.getKey();
const decryption = new global.Olm.PkDecryption();
try {
@@ -711,7 +720,7 @@ export class Curve25519 implements BackupAlgorithm {
public async keyMatches(key: Uint8Array): Promise<boolean> {
const decryption = new global.Olm.PkDecryption();
let pubKey;
let pubKey: string;
try {
pubKey = decryption.init_with_private_key(key);
} finally {
@@ -727,18 +736,9 @@ export class Curve25519 implements BackupAlgorithm {
}
function randomBytes(size: number): Uint8Array {
const crypto: {randomBytes: (n: number) => Uint8Array} | undefined = getCrypto() as any;
if (crypto) {
// nodejs version
return crypto.randomBytes(size);
}
if (window?.crypto) {
// browser version
const buf = new Uint8Array(size);
window.crypto.getRandomValues(buf);
return buf;
}
throw new Error("No usable crypto implementation");
const buf = new Uint8Array(size);
crypto.getRandomValues(buf);
return buf;
}
const UNSTABLE_MSC3270_NAME = new UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2");
@@ -807,7 +807,9 @@ export class Aes256 implements BackupAlgorithm {
return encryptAES(JSON.stringify(plainText), this.key, data.session_id);
}
public async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<IMegolmSessionData[]> {
public async decryptSessions(
sessions: Record<string, IKeyBackupSession<IEncryptedPayload>>,
): Promise<IMegolmSessionData[]> {
const keys: IMegolmSessionData[] = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
+50
View File
@@ -0,0 +1,50 @@
/*
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 { logger } from "../logger";
export let crypto = global.window?.crypto;
export let subtleCrypto = global.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
export let TextEncoder = global.window?.TextEncoder;
/* eslint-disable @typescript-eslint/no-var-requires */
if (!crypto) {
try {
crypto = require("crypto").webcrypto;
} catch (e) {
logger.error("Failed to load webcrypto", e);
}
}
if (!subtleCrypto) {
subtleCrypto = crypto?.subtle;
}
if (!TextEncoder) {
try {
TextEncoder = require("util").TextEncoder;
} catch (e) {
logger.error("Failed to load TextEncoder util", e);
}
}
/* eslint-enable @typescript-eslint/no-var-requires */
export function setCrypto(_crypto: Crypto): void {
crypto = _crypto;
subtleCrypto = _crypto.subtle ?? _crypto.webkitSubtle;
}
export function setTextEncoder(_TextEncoder: typeof TextEncoder): void {
TextEncoder = _TextEncoder;
}
-2
View File
@@ -210,7 +210,6 @@ export class DehydrationManager {
logger.log("Uploading account to server");
// eslint-disable-next-line camelcase
const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
undefined,
Method.Put,
"/dehydrated_device",
undefined,
@@ -273,7 +272,6 @@ export class DehydrationManager {
logger.log("Uploading keys to server");
await this.crypto.baseApis.http.authedRequest(
undefined,
Method.Post,
"/keys/upload/" + encodeURI(deviceId),
undefined,
+2 -32
View File
@@ -15,10 +15,7 @@ limitations under the License.
*/
import { randomString } from '../randomstring';
import { getCrypto } from '../utils';
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
import { subtleCrypto, TextEncoder } from "./crypto";
const DEFAULT_ITERATIONS = 500000;
@@ -75,21 +72,8 @@ export async function deriveKey(
iterations: number,
numBits = DEFAULT_BITSIZE,
): Promise<Uint8Array> {
return subtleCrypto
? deriveKeyBrowser(password, salt, iterations, numBits)
: deriveKeyNode(password, salt, iterations, numBits);
}
async function deriveKeyBrowser(
password: string,
salt: string,
iterations: number,
numBits: number,
): Promise<Uint8Array> {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
throw new Error("Password-based backup is not avaiable on this platform");
throw new Error("Password-based backup is not available on this platform");
}
const key = await subtleCrypto.importKey(
@@ -113,17 +97,3 @@ async function deriveKeyBrowser(
return new Uint8Array(keybits);
}
async function deriveKeyNode(
password: string,
salt: string,
iterations: number,
numBits: number,
): Promise<Uint8Array> {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
return crypto.pbkdf2Sync(password, Buffer.from(salt, 'binary'), iterations, numBits, 'sha512');
}
+6 -6
View File
@@ -23,18 +23,18 @@ export interface Curve25519SessionData {
mac: string;
}
export interface IKeyBackupSession {
first_message_index: number; // eslint-disable-line camelcase
forwarded_count: number; // eslint-disable-line camelcase
is_verified: boolean; // eslint-disable-line camelcase
session_data: Curve25519SessionData | IEncryptedPayload; // eslint-disable-line camelcase
/* eslint-disable camelcase */
export interface IKeyBackupSession<T = Curve25519SessionData | IEncryptedPayload> {
first_message_index: number;
forwarded_count: number;
is_verified: boolean;
session_data: T;
}
export interface IKeyBackupRoomSessions {
[sessionId: string]: IKeyBackupSession;
}
/* eslint-disable camelcase */
export interface ICurve25519AuthData {
public_key: string;
private_key_salt?: string;
+24 -5
View File
@@ -24,8 +24,9 @@ import { IDevice } from "../deviceinfo";
import { ICrossSigningInfo } from "../CrossSigning";
import { PrefixedLogger } from "../../logger";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
import { MatrixEvent } from "../../models/event";
import { DehydrationManager } from "../dehydration";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. Definitions for storage for the crypto module
@@ -33,6 +34,16 @@ import { MatrixEvent } from "../../models/event";
* @module
*/
export interface SecretStorePrivateKeys {
dehydration: {
keyInfo: DehydrationManager["keyInfo"];
key: IEncryptedPayload;
deviceDisplayName: string;
time: number;
} | null;
"m.megolm_backup.v1": IEncryptedPayload;
}
/**
* Abstraction of things that can store data required for end-to-end encryption
*
@@ -58,12 +69,20 @@ export interface CryptoStore {
deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise<OutgoingRoomKeyRequest | null>;
// Olm Account
getAccount(txn: unknown, func: (accountPickle: string) => void);
getAccount(txn: unknown, func: (accountPickle: string | null) => void);
storeAccount(txn: unknown, accountPickle: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void;
getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void;
storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void;
storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
key: SecretStorePrivateKeys[K],
): void;
// Olm Sessions
countEndToEndSessions(txn: unknown, func: (count: number) => void): void;
@@ -25,16 +25,14 @@ import {
IWithheld,
Mode,
OutgoingRoomKeyRequest,
ParkedSharedHistory,
ParkedSharedHistory, SecretStorePrivateKeys,
} from "./base";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
export const VERSION = 11;
const PROFILE_TRANSACTIONS = false;
/**
@@ -370,7 +368,7 @@ export class Backend implements CryptoStore {
// Olm Account
public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void): void {
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
@@ -387,7 +385,10 @@ export class Backend implements CryptoStore {
objectStore.put(accountPickle, "-");
}
public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
public getCrossSigningKeys(
txn: IDBTransaction,
func: (keys: Record<string, ICrossSigningKey> | null) => void,
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("crossSigningKeys");
getReq.onsuccess = function() {
@@ -399,10 +400,10 @@ export class Backend implements CryptoStore {
};
}
public getSecretStorePrivateKey(
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => void,
type: string,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
@@ -420,7 +421,11 @@ export class Backend implements CryptoStore {
objectStore.put(keys, "crossSigningKeys");
}
public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
type: K,
key: SecretStorePrivateKeys[K],
): void {
const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`);
}
@@ -950,45 +955,34 @@ export class Backend implements CryptoStore {
}
}
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
if (oldVersion < 1) { // The database did not previously exist.
createDatabase(db);
}
if (oldVersion < 2) {
db.createObjectStore("account");
}
if (oldVersion < 3) {
type DbMigration = (db: IDBDatabase) => void;
const DB_MIGRATIONS: DbMigration[] = [
(db) => { createDatabase(db); },
(db) => { db.createObjectStore("account"); },
(db) => {
const sessionsStore = db.createObjectStore("sessions", {
keyPath: ["deviceKey", "sessionId"],
});
sessionsStore.createIndex("deviceKey", "deviceKey");
}
if (oldVersion < 4) {
},
(db) => {
db.createObjectStore("inbound_group_sessions", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 5) {
db.createObjectStore("device_data");
}
if (oldVersion < 6) {
db.createObjectStore("rooms");
}
if (oldVersion < 7) {
},
(db) => { db.createObjectStore("device_data"); },
(db) => { db.createObjectStore("rooms"); },
(db) => {
db.createObjectStore("sessions_needing_backup", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 8) {
},
(db) => {
db.createObjectStore("inbound_group_sessions_withheld", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 9) {
},
(db) => {
const problemsStore = db.createObjectStore("session_problems", {
keyPath: ["deviceKey", "time"],
});
@@ -997,18 +991,29 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
db.createObjectStore("notified_error_devices", {
keyPath: ["userId", "deviceId"],
});
}
if (oldVersion < 10) {
},
(db) => {
db.createObjectStore("shared_history_inbound_group_sessions", {
keyPath: ["roomId"],
});
}
if (oldVersion < 11) {
},
(db) => {
db.createObjectStore("parked_shared_history", {
keyPath: ["roomId"],
});
}
},
// Expand as needed.
];
export const VERSION = DB_MIGRATIONS.length;
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
DB_MIGRATIONS.forEach((migration, index) => {
if (oldVersion <= index) migration(db);
});
}
function createDatabase(db: IDBDatabase): void {
+14 -8
View File
@@ -29,14 +29,13 @@ import {
IWithheld,
Mode,
OutgoingRoomKeyRequest,
ParkedSharedHistory,
ParkedSharedHistory, SecretStorePrivateKeys,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. indexeddb storage for e2e.
@@ -323,7 +322,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle
*/
public getAccount(txn: IDBTransaction, func: (accountPickle: string) => void) {
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void) {
this.backend.getAccount(txn, func);
}
@@ -346,7 +345,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(string)} func Called with the account keys object:
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
*/
public getCrossSigningKeys(txn: IDBTransaction, func: (keys: Record<string, ICrossSigningKey>) => void): void {
public getCrossSigningKeys(
txn: IDBTransaction,
func: (keys: Record<string, ICrossSigningKey> | null) => void,
): void {
this.backend.getCrossSigningKeys(txn, func);
}
@@ -355,10 +357,10 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {function(string)} func Called with the private key
* @param {string} type A key type
*/
public getSecretStorePrivateKey(
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
func: (key: IEncryptedPayload | null) => void,
type: string,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
this.backend.getSecretStorePrivateKey(txn, func, type);
}
@@ -380,7 +382,11 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys()
*/
public storeSecretStorePrivateKey(txn: IDBTransaction, type: string, key: IEncryptedPayload): void {
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: IDBTransaction,
type: K,
key: SecretStorePrivateKeys[K],
): void {
this.backend.storeSecretStorePrivateKey(txn, type, key);
}
+14 -7
View File
@@ -16,12 +16,11 @@ limitations under the License.
import { logger } from '../../logger';
import { MemoryCryptoStore } from './memory-crypto-store';
import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode } from "./base";
import { IDeviceData, IProblem, ISession, ISessionInfo, IWithheld, Mode, SecretStorePrivateKeys } from "./base";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { ICrossSigningKey } from "../../client";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. Partial localStorage backed storage for e2e.
@@ -374,7 +373,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
// Olm account
public getAccount(txn: unknown, func: (accountPickle: string) => void): void {
public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
const accountPickle = getJsonItem<string>(this.store, KEY_END_TO_END_ACCOUNT);
func(accountPickle);
}
@@ -383,13 +382,17 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
}
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void {
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
func(keys);
}
public getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void {
const key = getJsonItem<IEncryptedPayload>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
const key = getJsonItem<SecretStorePrivateKeys[K]>(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key);
}
@@ -397,7 +400,11 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
}
public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void {
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
key: SecretStorePrivateKeys[K],
): void {
setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
}
+17 -10
View File
@@ -25,14 +25,13 @@ import {
IWithheld,
Mode,
OutgoingRoomKeyRequest,
ParkedSharedHistory,
ParkedSharedHistory, SecretStorePrivateKeys,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes";
/**
* Internal module. in-memory storage for e2e.
@@ -45,9 +44,9 @@ import { IEncryptedPayload } from "../aes";
*/
export class MemoryCryptoStore implements CryptoStore {
private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
private account: string = null;
private crossSigningKeys: Record<string, ICrossSigningKey> = null;
private privateKeys: Record<string, IEncryptedPayload> = {};
private account: string | null = null;
private crossSigningKeys: Record<string, ICrossSigningKey> | null = null;
private privateKeys: Partial<SecretStorePrivateKeys> = {};
private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
private sessionProblems: { [deviceKey: string]: IProblem[] } = {};
@@ -280,7 +279,7 @@ export class MemoryCryptoStore implements CryptoStore {
// Olm Account
public getAccount(txn: unknown, func: (accountPickle: string) => void) {
public getAccount(txn: unknown, func: (accountPickle: string | null) => void) {
func(this.account);
}
@@ -288,12 +287,16 @@ export class MemoryCryptoStore implements CryptoStore {
this.account = accountPickle;
}
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey>) => void): void {
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
func(this.crossSigningKeys);
}
public getSecretStorePrivateKey(txn: unknown, func: (key: IEncryptedPayload | null) => void, type: string): void {
const result = this.privateKeys[type];
public getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void {
const result = this.privateKeys[type] as SecretStorePrivateKeys[K] | undefined;
func(result || null);
}
@@ -301,7 +304,11 @@ export class MemoryCryptoStore implements CryptoStore {
this.crossSigningKeys = keys;
}
public storeSecretStorePrivateKey(txn: unknown, type: string, key: IEncryptedPayload): void {
public storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
key: SecretStorePrivateKeys[K],
): void {
this.privateKeys[type] = key;
}
+62
View File
@@ -0,0 +1,62 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IServerVersions } from "./client";
export enum ServerSupport {
Stable,
Unstable,
Unsupported
}
export enum Feature {
Thread = "Thread",
ThreadUnreadNotifications = "ThreadUnreadNotifications",
}
type FeatureSupportCondition = {
unstablePrefixes?: string[];
matrixVersion?: string;
};
const featureSupportResolver: Record<string, FeatureSupportCondition> = {
[Feature.Thread]: {
unstablePrefixes: ["org.matrix.msc3440"],
matrixVersion: "v1.3",
},
[Feature.ThreadUnreadNotifications]: {
unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"],
matrixVersion: "v1.4",
},
};
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
const supportMap = new Map<Feature, ServerSupport>();
for (const [feature, supportCondition] of Object.entries(featureSupportResolver)) {
const supportMatrixVersion = versions.versions?.includes(supportCondition.matrixVersion || "") ?? false;
const supportUnstablePrefixes = supportCondition.unstablePrefixes?.every(unstablePrefix => {
return versions.unstable_features?.[unstablePrefix] === true;
}) ?? false;
if (supportMatrixVersion) {
supportMap.set(feature as Feature, ServerSupport.Stable);
} else if (supportUnstablePrefixes) {
supportMap.set(feature as Feature, ServerSupport.Unstable);
} else {
supportMap.set(feature as Feature, ServerSupport.Unsupported);
}
}
return supportMap;
}
+1 -1
View File
@@ -73,7 +73,7 @@ export interface IFilterComponent {
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
*/
export class FilterComponent {
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {}
constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
/**
* Checks with the filter component matches the given event
+40 -14
View File
@@ -22,6 +22,7 @@ import {
EventType,
RelationType,
} from "./@types/event";
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
import { FilterComponent, IFilterComponent } from "./filter-component";
import { MatrixEvent } from "./models/event";
@@ -57,6 +58,8 @@ export interface IRoomEventFilter extends IFilterComponent {
types?: Array<EventType | string>;
related_by_senders?: Array<RelationType | string>;
related_by_rel_types?: string[];
unread_thread_notifications?: boolean;
"org.matrix.msc3773.unread_thread_notifications"?: boolean;
// Unstable values
"io.element.relation_senders"?: Array<RelationType | string>;
@@ -97,23 +100,23 @@ export class Filter {
* @param {Object} jsonObj
* @return {Filter}
*/
public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter {
const filter = new Filter(userId, filterId);
filter.setDefinition(jsonObj);
return filter;
}
private definition: IFilterDefinition = {};
private roomFilter: FilterComponent;
private roomTimelineFilter: FilterComponent;
private roomFilter?: FilterComponent;
private roomTimelineFilter?: FilterComponent;
constructor(public readonly userId: string, public filterId?: string) {}
constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?string} The filter ID
*/
getFilterId(): string | null {
public getFilterId(): string | undefined {
return this.filterId;
}
@@ -121,7 +124,7 @@ export class Filter {
* Get the JSON body of the filter.
* @return {Object} The filter definition
*/
getDefinition(): IFilterDefinition {
public getDefinition(): IFilterDefinition {
return this.definition;
}
@@ -129,7 +132,7 @@ export class Filter {
* Set the JSON body of the filter
* @param {Object} definition The filter definition
*/
setDefinition(definition: IFilterDefinition) {
public setDefinition(definition: IFilterDefinition) {
this.definition = definition;
// This is all ported from synapse's FilterCollection()
@@ -198,7 +201,7 @@ export class Filter {
* Get the room.timeline filter component of the filter
* @return {FilterComponent} room timeline filter component
*/
getRoomTimelineFilterComponent(): FilterComponent {
public getRoomTimelineFilterComponent(): FilterComponent | undefined {
return this.roomTimelineFilter;
}
@@ -208,20 +211,43 @@ export class Filter {
* @param {MatrixEvent[]} events the list of events being filtered
* @return {MatrixEvent[]} the list of events which match the filter
*/
filterRoomTimeline(events: MatrixEvent[]): MatrixEvent[] {
return this.roomTimelineFilter.filter(this.roomFilter.filter(events));
public filterRoomTimeline(events: MatrixEvent[]): MatrixEvent[] {
if (this.roomFilter) {
events = this.roomFilter.filter(events);
}
if (this.roomTimelineFilter) {
events = this.roomTimelineFilter.filter(events);
}
return events;
}
/**
* Set the max number of events to return for each room's timeline.
* @param {Number} limit The max number of events to return for each room.
*/
setTimelineLimit(limit: number) {
public setTimelineLimit(limit: number) {
setProp(this.definition, "room.timeline.limit", limit);
}
setLazyLoadMembers(enabled: boolean) {
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
/**
* Enable threads unread notification
* @param {boolean} enabled
*/
public setUnreadThreadNotifications(enabled: boolean): void {
this.definition = {
...this.definition,
room: {
...this.definition?.room,
timeline: {
...this.definition?.room?.timeline,
[UNREAD_THREAD_NOTIFICATIONS.name]: enabled,
},
},
};
}
public setLazyLoadMembers(enabled: boolean): void {
setProp(this.definition, "room.state.lazy_load_members", enabled);
}
/**
@@ -229,7 +255,7 @@ export class Filter {
* @param {boolean} includeLeave True to make rooms the user has left appear
* in responses.
*/
setIncludeLeaveRooms(includeLeave: boolean) {
public setIncludeLeaveRooms(includeLeave: boolean) {
setProp(this.definition, "room.include_leave", includeLeave);
}
}
-1140
View File
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
/*
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 { IUsageLimit } from "../@types/partials";
interface IErrorJson extends Partial<IUsageLimit> {
[key: string]: any; // extensible
errcode?: string;
error?: string;
}
/**
* Construct a generic HTTP error. This is a JavaScript Error with additional information
* specific to HTTP responses.
* @constructor
* @param {string} msg The error message to include.
* @param {number} httpStatus The HTTP response status code.
*/
export class HTTPError extends Error {
constructor(msg: string, public readonly httpStatus?: number) {
super(msg);
}
}
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response.
* @constructor
* @param {Object} errorJson The Matrix error JSON returned from the homeserver.
* @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
* @prop {Object} data The raw Matrix error JSON used to construct this object.
* @prop {number} httpStatus The numeric HTTP status code given
*/
export class MatrixError extends HTTPError {
public readonly errcode?: string;
public readonly data: IErrorJson;
constructor(errorJson: IErrorJson = {}, public readonly httpStatus?: number, public url?: string) {
let message = errorJson.error || "Unknown message";
if (httpStatus) {
message = `[${httpStatus}] ${message}`;
}
if (url) {
message = `${message} (${url})`;
}
super(`MatrixError: ${message}`, httpStatus);
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.data = errorJson;
}
}
/**
* Construct a ConnectionError. This is a JavaScript Error indicating
* that a request failed because of some error with the connection, either
* CORS was not correctly configured on the server, the server didn't response,
* the request timed out, or the internet connection on the client side went down.
* @constructor
*/
export class ConnectionError extends Error {
constructor(message: string, cause?: Error) {
super(message + (cause ? `: ${cause.message}` : ""));
}
get name() {
return "ConnectionError";
}
}
+329
View File
@@ -0,0 +1,329 @@
/*
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.
*/
/**
* This is an internal module. See {@link MatrixHttpApi} for the public class.
* @module http-api
*/
import * as utils from "../utils";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { Method } from "./method";
import { ConnectionError, MatrixError } from "./errors";
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, IRequestOpts } from "./interface";
import { anySignal, parseErrorResponse, timeoutSignal } from "./utils";
import { QueryDict } from "../utils";
type Body = Record<string, any> | BodyInit;
interface TypedResponse<T> extends Response {
json(): Promise<T>;
}
export type ResponseType<T, O extends IHttpOpts> =
O extends undefined ? T :
O extends { onlyData: true } ? T :
TypedResponse<T>;
export class FetchHttpApi<O extends IHttpOpts> {
private abortController = new AbortController();
constructor(
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
public readonly opts: O,
) {
utils.checkObjectHasKeys(opts, ["baseUrl", "prefix"]);
opts.onlyData = !!opts.onlyData;
opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true;
}
public abort(): void {
this.abortController.abort();
this.abortController = new AbortController();
}
public fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
if (this.opts.fetchFn) {
return this.opts.fetchFn(resource, options);
}
return global.fetch(resource, options);
}
/**
* Sets the base URL for the identity server
* @param {string} url The new base url
*/
public setIdBaseUrl(url: string): void {
this.opts.idBaseUrl = url;
}
public idServerRequest<T extends {}>(
method: Method,
path: string,
params: Record<string, string | string[]>,
prefix: string,
accessToken?: string,
): Promise<ResponseType<T, O>> {
if (!this.opts.idBaseUrl) {
throw new Error("No identity server base URL set");
}
let queryParams: QueryDict | undefined = undefined;
let body: Record<string, string | string[]> | undefined = undefined;
if (method === Method.Get) {
queryParams = params;
} else {
body = params;
}
const fullUri = this.getUrl(path, queryParams, prefix, this.opts.idBaseUrl);
const opts: IRequestOpts = {
json: true,
headers: {},
};
if (accessToken) {
opts.headers.Authorization = `Bearer ${accessToken}`;
}
return this.requestOtherUrl(method, fullUri, body, opts);
}
/**
* Perform an authorised request to the homeserver.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} [body] The HTTP JSON body.
*
* @param {Object|Number=} opts additional options. If a number is specified,
* this is treated as `opts.localTimeoutMs`.
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {string=} opts.baseUrl The alternative base url to use.
* If not specified, uses this.opts.baseUrl
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
public authedRequest<T>(
method: Method,
path: string,
queryParams?: QueryDict,
body?: Body,
opts: IRequestOpts = {},
): Promise<ResponseType<T, O>> {
if (!queryParams) queryParams = {};
if (this.opts.accessToken) {
if (this.opts.useAuthorizationHeader) {
if (!opts.headers) {
opts.headers = {};
}
if (!opts.headers.Authorization) {
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
}
if (queryParams.access_token) {
delete queryParams.access_token;
}
} else if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
}
const requestPromise = this.request<T>(method, path, queryParams, body, opts);
requestPromise.catch((err: MatrixError) => {
if (err.errcode == 'M_UNKNOWN_TOKEN' && !opts?.inhibitLogoutEmit) {
this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err);
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri);
}
});
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return requestPromise;
}
/**
* Perform a request to the homeserver without any credentials.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} [body] The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {string=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
public request<T>(
method: Method,
path: string,
queryParams?: QueryDict,
body?: Body,
opts?: IRequestOpts,
): Promise<ResponseType<T, O>> {
const fullUri = this.getUrl(path, queryParams, opts?.prefix, opts?.baseUrl);
return this.requestOtherUrl<T>(method, fullUri, body, opts);
}
/**
* Perform a request to an arbitrary URL.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} url The HTTP URL object.
*
* @param {Object} [body] The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {Promise} Resolves to data unless `onlyData` is specified as false,
* where the resolved value will be a fetch Response object.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
public async requestOtherUrl<T>(
method: Method,
url: URL | string,
body?: Body,
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "abortSignal"> = {},
): Promise<ResponseType<T, O>> {
const headers = Object.assign({}, opts.headers || {});
const json = opts.json ?? true;
// We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref
const jsonBody = json && body?.constructor?.name === Object.name;
if (json) {
if (jsonBody && !headers["Content-Type"]) {
headers["Content-Type"] = "application/json";
}
if (!headers["Accept"]) {
headers["Accept"] = "application/json";
}
}
const timeout = opts.localTimeoutMs ?? this.opts.localTimeoutMs;
const signals = [
this.abortController.signal,
];
if (timeout !== undefined) {
signals.push(timeoutSignal(timeout));
}
if (opts.abortSignal) {
signals.push(opts.abortSignal);
}
let data: BodyInit;
if (jsonBody) {
data = JSON.stringify(body);
} else {
data = body as BodyInit;
}
const { signal, cleanup } = anySignal(signals);
let res: Response;
try {
res = await this.fetch(url, {
signal,
method,
body: data,
headers,
mode: "cors",
redirect: "follow",
referrer: "",
referrerPolicy: "no-referrer",
cache: "no-cache",
credentials: "omit", // we send credentials via headers
});
} catch (e) {
if (e.name === "AbortError") {
throw e;
}
throw new ConnectionError("fetch failed", e);
} finally {
cleanup();
}
if (!res.ok) {
throw parseErrorResponse(res, await res.text());
}
if (this.opts.onlyData) {
return json ? res.json() : res.text();
}
return res as ResponseType<T, O>;
}
/**
* Form and return a homeserver request URL based on the given path params and prefix.
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g. "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be urlencoded).
* @param {string} prefix The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix.
* @param {string} baseUrl The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl.
* @return {string} URL
*/
public getUrl(
path: string,
queryParams?: QueryDict,
prefix?: string,
baseUrl?: string,
): URL {
const url = new URL((baseUrl ?? this.opts.baseUrl) + (prefix ?? this.opts.prefix) + path);
if (queryParams) {
utils.encodeParams(queryParams, url.searchParams);
}
return url;
}
}
+205
View File
@@ -0,0 +1,205 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { FetchHttpApi } from "./fetch";
import { FileType, IContentUri, IHttpOpts, Upload, UploadOpts, UploadResponse } from "./interface";
import { MediaPrefix } from "./prefix";
import * as utils from "../utils";
import * as callbacks from "../realtime-callbacks";
import { Method } from "./method";
import { ConnectionError } from "./errors";
import { parseErrorResponse } from "./utils";
export * from "./interface";
export * from "./prefix";
export * from "./errors";
export * from "./method";
export * from "./utils";
export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
private uploads: Upload[] = [];
/**
* Upload content to the homeserver
*
* @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a Buffer, String or ReadStream.
*
* @param {object} opts options object
*
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {boolean=} opts.includeFilename if false will not send the filename,
* e.g for encrypted file uploads where filename leaks are undesirable.
* Defaults to true.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>application/octet-stream</tt>.
*
* @param {Function=} opts.progressHandler Optional. Called when a chunk of
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*
* @return {Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
public uploadContent(file: FileType, opts: UploadOpts = {}): Promise<UploadResponse> {
const includeFilename = opts.includeFilename ?? true;
const abortController = opts.abortController ?? new AbortController();
// If the file doesn't have a mime type, use a default since the HS errors if we don't supply one.
const contentType = opts.type ?? (file as File).type ?? 'application/octet-stream';
const fileName = opts.name ?? (file as File).name;
const upload = {
loaded: 0,
total: 0,
abortController,
} as Upload;
const defer = utils.defer<UploadResponse>();
if (global.XMLHttpRequest) {
const xhr = new global.XMLHttpRequest();
const timeoutFn = function() {
xhr.abort();
defer.reject(new Error("Timeout"));
};
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(timeoutTimer);
try {
if (xhr.status === 0) {
throw new DOMException(xhr.statusText, "AbortError"); // mimic fetch API
}
if (!xhr.responseText) {
throw new Error('No response body.');
}
if (xhr.status >= 400) {
defer.reject(parseErrorResponse(xhr, xhr.responseText));
} else {
defer.resolve(JSON.parse(xhr.responseText));
}
} catch (err) {
if ((<Error>err).name === "AbortError") {
defer.reject(err);
return;
}
defer.reject(new ConnectionError("request failed", <Error>err));
}
break;
}
};
xhr.upload.onprogress = (ev: ProgressEvent) => {
callbacks.clearTimeout(timeoutTimer);
upload.loaded = ev.loaded;
upload.total = ev.total;
timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
opts.progressHandler?.({
loaded: ev.loaded,
total: ev.total,
});
};
const url = this.getUrl("/upload", undefined, MediaPrefix.R0);
if (includeFilename && fileName) {
url.searchParams.set("filename", encodeURIComponent(fileName));
}
if (!this.opts.useAuthorizationHeader && this.opts.accessToken) {
url.searchParams.set("access_token", encodeURIComponent(this.opts.accessToken));
}
xhr.open(Method.Post, url.href);
if (this.opts.useAuthorizationHeader && this.opts.accessToken) {
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
}
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(file);
abortController.signal.addEventListener("abort", () => {
xhr.abort();
});
} else {
const queryParams: utils.QueryDict = {};
if (includeFilename && fileName) {
queryParams.filename = fileName;
}
const headers: Record<string, string> = { "Content-Type": contentType };
this.authedRequest<UploadResponse>(
Method.Post, "/upload", queryParams, file, {
prefix: MediaPrefix.R0,
headers,
abortSignal: abortController.signal,
},
).then(response => {
return this.opts.onlyData ? <UploadResponse>response : response.json();
}).then(defer.resolve, defer.reject);
}
// remove the upload from the list on completion
upload.promise = defer.promise.finally(() => {
utils.removeElement(this.uploads, elem => elem === upload);
});
abortController.signal.addEventListener("abort", () => {
utils.removeElement(this.uploads, elem => elem === upload);
defer.reject(new DOMException("Aborted", "AbortError"));
});
this.uploads.push(upload);
return upload.promise;
}
public cancelUpload(promise: Promise<UploadResponse>): boolean {
const upload = this.uploads.find(u => u.promise === promise);
if (upload) {
upload.abortController.abort();
return true;
}
return false;
}
public getCurrentUploads(): Upload[] {
return this.uploads;
}
/**
* Get the content repository url with query parameters.
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
* path and query parameters respectively.
*/
public getContentUri(): IContentUri {
return {
base: this.opts.baseUrl,
path: MediaPrefix.R0 + "/upload",
params: {
access_token: this.opts.accessToken!,
},
};
}
}
+93
View File
@@ -0,0 +1,93 @@
/*
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 { MatrixError } from "./errors";
export interface IHttpOpts {
fetchFn?: typeof global.fetch;
baseUrl: string;
idBaseUrl?: string;
prefix: string;
extraParams?: Record<string, string>;
accessToken?: string;
useAuthorizationHeader?: boolean; // defaults to true
onlyData?: boolean;
localTimeoutMs?: number;
}
export interface IRequestOpts {
baseUrl?: string;
prefix?: string;
headers?: Record<string, string>;
abortSignal?: AbortSignal;
localTimeoutMs?: number;
json?: boolean; // defaults to true
// Set to true to prevent the request function from emitting a Session.logged_out event.
// This is intended for use on endpoints where M_UNKNOWN_TOKEN is a valid/notable error response,
// such as with token refreshes.
inhibitLogoutEmit?: boolean;
}
export interface IContentUri {
base: string;
path: string;
params: {
// eslint-disable-next-line camelcase
access_token: string;
};
}
export enum HttpApiEvent {
SessionLoggedOut = "Session.logged_out",
NoConsent = "no_consent",
}
export type HttpApiEventHandlerMap = {
[HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void;
[HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void;
};
export interface UploadProgress {
loaded: number;
total: number;
}
export interface UploadOpts {
name?: string;
type?: string;
includeFilename?: boolean;
progressHandler?(progress: UploadProgress): void;
abortController?: AbortController;
}
export interface Upload {
loaded: number;
total: number;
promise: Promise<UploadResponse>;
abortController: AbortController;
}
export interface UploadResponse {
// eslint-disable-next-line camelcase
content_uri: string;
}
export type FileType = XMLHttpRequestBodyInit;
+22
View File
@@ -0,0 +1,22 @@
/*
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.
*/
export enum Method {
Get = "GET",
Put = "PUT",
Post = "POST",
Delete = "DELETE",
}
+53
View File
@@ -0,0 +1,53 @@
/*
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.
*/
export enum ClientPrefix {
/**
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
*/
R0 = "/_matrix/client/r0",
/**
* A constant representing the URI path for the legacy release v1 of the Client-Server HTTP API.
*/
V1 = "/_matrix/client/v1",
/**
* A constant representing the URI path for Client-Server API endpoints versioned at v3.
*/
V3 = "/_matrix/client/v3",
/**
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
*/
Unstable = "/_matrix/client/unstable",
}
export enum IdentityPrefix {
/**
* URI path for v1 of the identity API
* @deprecated Use v2.
*/
V1 = "/_matrix/identity/api/v1",
/**
* URI path for the v2 identity API
*/
V2 = "/_matrix/identity/v2",
}
export enum MediaPrefix {
/**
* URI path for the media repo API
*/
R0 = "/_matrix/media/r0",
}
+153
View File
@@ -0,0 +1,153 @@
/*
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 { parse as parseContentType, ParsedMediaType } from "content-type";
import { logger } from "../logger";
import { sleep } from "../utils";
import { ConnectionError, HTTPError, MatrixError } from "./errors";
// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
export function timeoutSignal(ms: number): AbortSignal {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, ms);
return controller.signal;
}
export function anySignal(signals: AbortSignal[]): {
signal: AbortSignal;
cleanup(): void;
} {
const controller = new AbortController();
function cleanup() {
for (const signal of signals) {
signal.removeEventListener("abort", onAbort);
}
}
function onAbort() {
controller.abort();
cleanup();
}
for (const signal of signals) {
if (signal.aborted) {
onAbort();
break;
}
signal.addEventListener("abort", onAbort);
}
return {
signal: controller.signal,
cleanup,
};
}
/**
* Attempt to turn an HTTP error response into a Javascript Error.
*
* If it is a JSON response, we will parse it into a MatrixError. Otherwise
* we return a generic Error.
*
* @param {XMLHttpRequest|Response} response response object
* @param {String} body raw body of the response
* @returns {Error}
*/
export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error {
let contentType: ParsedMediaType;
try {
contentType = getResponseContentType(response);
} catch (e) {
return e;
}
if (contentType?.type === "application/json" && body) {
return new MatrixError(
JSON.parse(body),
response.status,
isXhr(response) ? response.responseURL : response.url,
);
}
if (contentType?.type === "text/plain") {
return new HTTPError(`Server returned ${response.status} error: ${body}`, response.status);
}
return new HTTPError(`Server returned ${response.status} error`, response.status);
}
function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest {
return "getResponseHeader" in response;
}
/**
* extract the Content-Type header from the response object, and
* parse it to a `{type, parameters}` object.
*
* returns null if no content-type header could be found.
*
* @param {XMLHttpRequest|Response} response response object
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
*/
function getResponseContentType(response: XMLHttpRequest | Response): ParsedMediaType | null {
let contentType: string | null;
if (isXhr(response)) {
contentType = response.getResponseHeader("Content-Type");
} else {
contentType = response.headers.get("Content-Type");
}
if (!contentType) return null;
try {
return parseContentType(contentType);
} catch (e) {
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
}
}
/**
* Retries a network operation run in a callback.
* @param {number} maxAttempts maximum attempts to try
* @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again.
* @return {any} the result of the network operation
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
*/
export async function retryNetworkOperation<T>(maxAttempts: number, callback: () => Promise<T>): Promise<T> {
let attempts = 0;
let lastConnectionError: ConnectionError | null = null;
while (attempts < maxAttempts) {
try {
if (attempts > 0) {
const timeout = 1000 * Math.pow(2, attempts);
logger.log(`network operation failed ${attempts} times, retrying in ${timeout}ms...`);
await sleep(timeout);
}
return await callback();
} catch (err) {
if (err instanceof ConnectionError) {
attempts += 1;
lastConnectionError = err;
} else {
throw err;
}
}
}
throw lastConnectionError;
}
+2 -15
View File
@@ -14,25 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as request from "request";
import * as matrixcs from "./matrix";
import * as utils from "./utils";
import { logger } from './logger';
if (matrixcs.getRequest()) {
if (global.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
matrixcs.request(request);
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
logger.log('nodejs was compiled without crypto support');
}
global.__js_sdk_entrypoint = true;
export * from "./matrix";
export default matrixcs;

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