Compare commits

..

98 Commits

Author SHA1 Message Date
RiotRobot 1bf8533c03 v35.1.0 2024-12-18 14:10:31 +00:00
David Baker fb2f2dd6d4 Merge pull request #4593 from matrix-org/rav/migration-for-previouslyverified
Upgrade matrix-sdk-crypto-wasm to 1.11.0
2024-12-18 14:01:18 +00:00
Richard van der Hoff d33350ff26 Upgrade matrix-sdk-crypto-wasm to 1.11.0
... to fix https://github.com/matrix-org/matrix-rust-sdk/issues/4424
2024-12-18 13:29:05 +00:00
RiotRobot bee65ff13f v35.0.0 2024-12-17 13:22:10 +00:00
RiotRobot 3de2b9bf80 v35.0.0-rc.0 2024-12-10 15:43:43 +00:00
Hubert Chathi ded87290ce Update matrix-sdk-crypto-wasm to 11.0.0 (#4566)
* Update matrix-sdk-crypto-wasm to 11.0.0

* use `backend` variable to test for rust crypto

* apply changes from review
2024-12-09 23:11:02 +00:00
renovate[bot] cf39595bd7 Update dependency typedoc to v0.27.3 (#4573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 19:21:39 +00:00
Stanislav Demydiuk c90ea985c6 Fix age field check in event echo processing (#3635)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-12-05 16:08:41 +00:00
Florian Duros c54ca29aa8 Rename initCrypto into initLegacyCrypto (#4567) 2024-12-05 11:08:38 +00:00
Michael Telatynski beb3721e7a Avoid use of Buffer as it does not exist in the Web natively (#4569) 2024-12-04 22:32:09 +00:00
Richard van der Hoff 1cad6f4451 Add multiprocess health warnings to initRustCrypto (#4571) 2024-12-04 14:38:48 +00:00
RiotRobot c4ea57d42d Merge branch 'master' into develop 2024-12-03 12:24:07 +00:00
RiotRobot d3f5526ec0 v34.13.0 2024-12-03 12:23:40 +00:00
renovate[bot] b8e332b53d Update dependency typescript to v5.7.2 (#4553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 11:47:14 +00:00
renovate[bot] ab78acc7da Update typedoc (#4568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 11:19:56 +00:00
Michael Telatynski 051f4e2ab9 Fix release-checks to not use reserved name GITHUB_TOKEN
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 10:47:35 +00:00
Michael Telatynski 8863e42e35 More typescript linting (#3310)
* More typescript linting

* Improve types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Discard changes to src/models/MSC3089TreeSpace.ts

* Discard changes to src/realtime-callbacks.ts

* Fix tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 09:56:52 +00:00
Michael Telatynski bc5246970c Extract release sanity checks to reusable workflow (#4546)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 09:14:45 +00:00
Florian Duros edac6a9983 Replace deprecate imports (#4565)
* Replace deprecate imports

* Deprecate `CryptoBackend.getEventEncryptionInfo`

* Add deprecated alternative
2024-11-29 10:04:20 +00:00
Florian Duros 97ef1dc6df Remove deprecated calls of MatrixClient (#4563) 2024-11-29 09:49:52 +00:00
Florian Duros 125e45c24d Deprecate remaining legacy functions and move CryptoEvent.LegacyCryptoStoreMigrationProgress handler (#4560)
* Deprecate legacy functions in `MatrixClient`

* Move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler in rust crypto

* Remove `olmLib` usage in `MatrixClient`
2024-11-29 09:49:32 +00:00
Hugh Nimmo-Smith 3781b6ebfa Re-send MatrixRTC media encryption keys for a new joiner even if a rotation is in progress (#4561) 2024-11-28 12:05:39 +00:00
Michael Telatynski 8fc77c595a Move bump downstreams to js-sdk specific release workflow (#4547)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-27 14:15:51 +00:00
David Baker 5bcd26e506 Support MSC4222 state_after (#4487)
* WIP support for state_after

* Fix sliding sync sdk / embedded tests

* Allow both state & state_after to be undefined

Since it must have allowed state to be undefined previously: the test
had it as such.

* Fix limited sync handling

* Need to use state_after being undefined

if state can be undefined anyway

* Make sliding sync sdk tests pass

* Remove deprecated interfaces & backwards-compat code

* Remove useless assignment

* Use updates unstable prefix

* Clarify docs

* Remove additional semi-backwards compatible overload

* Update unstable prefixes

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test for MSC4222 behaviour

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add comments to explain why things work as they are.

* Fix sync accumulator for state_after sync handling

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)"

This reverts commit 957329b218.

* Fix Sync Accumulator toJSON putting start timeline state in state_after field

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test case

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Timo <toger5@hotmail.de>
2024-11-27 11:40:41 +00:00
Timo 66f099b2e7 Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)" (#4532)
This reverts commit 957329b218.
2024-11-27 10:49:29 +00:00
renovate[bot] b1445d7457 Update all non-major dependencies (#4558)
* Update all non-major dependencies

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-27 09:22:49 +00:00
Michael Telatynski b3794760c6 Merge remote-tracking branch 'origin/develop' into develop 2024-11-27 09:10:52 +00:00
Michael Telatynski fbdcc597b6 Fix workflow permissions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-27 07:44:35 +00:00
renovate[bot] 8ac32b7894 Update dependency typedoc-plugin-coverage to v3.4.0 (#4559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 18:51:22 +00:00
renovate[bot] b27c6de78d Update guibranco/github-status-action-v2 digest to 66088c4 (#4548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 18:23:16 +00:00
renovate[bot] c8e0987774 Update dependency typedoc-plugin-missing-exports to v3.0.2 (#4550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:41:22 +00:00
renovate[bot] 849fcd3341 Update dependency @types/node to v18.19.65 (#4549)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:33:27 +00:00
renovate[bot] 8df8266e4a Update dependency @stylistic/eslint-plugin to v2.11.0 (#4552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:16:47 +00:00
renovate[bot] cba4edfd84 Update dependency typedoc-plugin-mdn-links to v4 (#4556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:14:18 +00:00
renovate[bot] 4f50290cd6 Update all non-major dependencies (#4551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 16:35:48 +00:00
renovate[bot] 35e31f02ac Update typescript-eslint monorepo to v8.15.0 (#4554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 16:35:13 +00:00
Hubert Chathi 69647a33b6 Use shield status codes from Rust rather than string matching (#4529) 2024-11-26 15:06:57 +00:00
RiotRobot 13df897896 v34.13.0-rc.0 2024-11-26 13:42:27 +00:00
Michael Telatynski 006929ac0c Fix release workflow permissions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-26 13:41:14 +00:00
dependabot[bot] 405a6fbb92 Bump smol-toml from 1.3.0 to 1.3.1 (#4544)
Bumps [smol-toml](https://github.com/squirrelchat/smol-toml) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/commits)

---
updated-dependencies:
- dependency-name: smol-toml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 09:59:55 +00:00
Michael Telatynski d6f3262e12 Prevent releases when release blockers exist (#4543)
* Prevent releases when release blockers exist

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add permissions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-26 09:59:41 +00:00
Michael Telatynski 8b32f3eb7f Ensure we disambiguate display names which look like MXIDs (#4540)
* Ensure we disambiguate display names which look like MXIDs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Make tests clearer

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-22 15:52:50 +00:00
Michael Telatynski 1e9934a69d Fix docs-pr-netlify.yaml permissions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-22 11:35:04 +00:00
Michael Telatynski a6d342d3a4 Switch away from deprecated github-status-action (#4539)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 17:04:22 +00:00
Michael Telatynski 4b9a1bd53f Fix coverage check
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 16:05:38 +00:00
Michael Telatynski 26248f85d5 Additional check to ensure artifacts are downloaded
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 16:00:30 +00:00
Michael Telatynski ae2ae483db We have to explicitly specify github-token even though it is the default for this to work in theory
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 15:59:17 +00:00
Michael Telatynski c87692d0aa Tighten GITHUB_TOKEN permissions (#4538)
* Tighten GITHUB_TOKEN permissions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tighten GITHUB_TOKEN permissions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix permission

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-21 14:01:45 +00:00
Florian Duros 2dd4334e20 Fix RustBackupManager remaining values after current backup removal. #4534 (#4537) 2024-11-21 13:21:37 +00:00
dependabot[bot] 2210255d6f Bump cross-spawn from 7.0.3 to 7.0.6 (#4536)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-20 13:28:48 +00:00
RiotRobot 544ac86d20 Merge branch 'master' into develop 2024-11-19 14:18:23 +00:00
RiotRobot cf06547063 v34.12.0 2024-11-19 14:17:48 +00:00
Florian Duros 781c3b05e5 Add CryptoApi.getBackupInfo (#4512)
* Add `CryptoApi.getBackupInfo`

* improve doc
2024-11-18 13:31:35 +00:00
Timo 325dace437 Fix local echo in embedded mode (#4498)
* fix local echo

* dont use custome event emitter anymore

* move logic into updateTxId

* temp testing

* use generic eventEmtitter names

* add tests

---------

Co-authored-by: Robin <robin@robin.town>
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2024-11-14 13:21:20 +00:00
Michael Telatynski 5c894b34b3 Add downstream tsc for element-web (#4508)
* Add downstream tsc for element-web

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update static_analysis.yml

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-13 14:16:37 +00:00
Florian Duros f5f4091a00 Deprecate MatrixClient.isEventSenderVerified (#4527) 2024-11-13 11:10:38 +00:00
Florian Duros 52bdb57a47 Use crypto api import for CryptoCallbacks instead of legacy crypto (#4526) 2024-11-13 10:47:02 +00:00
Richard van der Hoff 3c23eb69b5 Remove reference to libolm from readme (#4525)
* Remove reference to libolm from readme

* README: fix link to node example
2024-11-13 10:24:49 +00:00
Florian Duros c93b7ce188 Add restoreKeybackup to CryptoApi. (#4476)
* First draft of moving out restoreKeyBackup out of MatrixClient

* Deprecate `restoreKeyBackup*` in `MatrixClient`

* Move types

* Handle only the room keys response

* Renaming and refactor `keysCountInBatch` & `getTotalKeyCount`

* Fix `importRoomKeysAsJson` tsdoc

* Fix typo

* Move `backupDecryptor.free()``

* Comment and simplify a bit `handleDecryptionOfAFullBackup`

* Fix decryption crash by moving`backupDecryptor.free`

* Use new api in `megolm-backup.spec.ts`

* Add tests to get recovery key from secret storage

* Add doc to `KeyBackupRestoreOpts` & `KeyBackupRestoreResult`

* Add doc to `restoreKeyBackupWithKey`

* Add doc to `backup.ts`

* Apply comment suggestions

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

* - Decryption key is recovered from the cache in `RustCrypto.restoreKeyBackup`
- Add `CryptoApi.getSecretStorageBackupPrivateKey` to get the decryption key from the secret storage.

* Add `CryptoApi.restoreKeyBackup` to `ImportRoomKeyProgressData` doc.

* Add deprecated symbol to all the `restoreKeyBackup*` overrides.

* Update tests

* Move `RustBackupManager.getTotalKeyCount` to `backup#calculateKeyCountInKeyBackup`

* Fix `RustBackupManager.restoreKeyBackup` tsdoc

* Move `backupDecryptor.free` in rust crypto.

* Move `handleDecryptionOfAFullBackup` in `importKeyBackup`

* Rename `calculateKeyCountInKeyBackup` to `countKeystInBackup`

* Fix `passphrase` typo

* Rename `backupInfoVersion` to `backupVersion`

* Complete restoreKeyBackup* methods documentation

* Add `loadSessionBackupPrivateKeyFromSecretStorage`

* Remove useless intermediary result variable.

* Check that decryption key matchs key backup info in `loadSessionBackupPrivateKeyFromSecretStorage`

* Get backup info from a specific version

* Fix typo in `countKeysInBackup`

* Improve documentation and naming

* Use `RustSdkCryptoJs.BackupDecryptionKey` as `decryptionKeyMatchesKeyBackupInfo` parameter.

* Call directly `olmMachine.getBackupKeys` in `restoreKeyBackup`

* Last review changes

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-11-13 09:17:32 +00:00
Florian Duros 705b6336cf Update e2e doc in README.md (#4503)
* Update e2e doc in `README.md`

* Update `ICreateClientOpts.cryptoStore` doc

* Apply first batch of suggestion

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

* Fix `cryptoStore` tsdoc in `client.ts`

* Changes in Initialization chapter.

* Add doc about deprecated methods in `MatrixClient`.

* Update SecretStorage doc

* Fis typos

* Improve e2e docs

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-11-12 16:51:01 +00:00
renovate[bot] 7c41e3fb06 Update all non-major dependencies (#4519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 15:47:11 +00:00
renovate[bot] a314e612fa Update dependency @stylistic/eslint-plugin to v2.10.1 (#4520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 15:07:51 +00:00
renovate[bot] ac6cad2852 Update babel monorepo (#4517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:29:01 +00:00
renovate[bot] 906390f0bf Update shogo82148/actions-upload-release-asset digest to 8482bd7 (#4516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:10:31 +00:00
renovate[bot] 795497fafa Update crazy-max/ghaction-import-gpg digest to cb9bde2 (#4515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:10:01 +00:00
renovate[bot] ffb777d118 Update typescript-eslint monorepo to v8.13.0 (#4521)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:08:01 +00:00
renovate[bot] fbba8a2d71 Update typedoc (#4518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:08:00 +00:00
renovate[bot] 053c5741b0 Update actions/checkout digest to 11bd719 (#4514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 14:07:59 +00:00
RiotRobot 620dc2f6e2 v34.12.0-rc.0 2024-11-12 13:59:57 +00:00
Michael Telatynski 00d78077b0 Fix tests after security patches (#4513)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-12 11:16:03 +00:00
RiotRobot 23d2d03912 Merge branch 'master' into develop 2024-11-12 09:30:35 +00:00
RiotRobot b4c4355d1a v34.11.1 2024-11-12 09:30:01 +00:00
Michael Telatynski 142c0a65e6 Merge remote-tracking branch 'origin/develop' into develop 2024-11-12 09:27:54 +00:00
Michael Telatynski b9aacea1cb Fix release scripts
Regressed by https://github.com/matrix-org/matrix-js-sdk/pull/4496

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-12 09:27:44 +00:00
RiotRobot 76e653b7ee Merge branch 'master' into develop 2024-11-12 09:13:57 +00:00
RiotRobot 4d4ff4c3f2 v34.11.0 2024-11-12 09:13:26 +00:00
Michael Telatynski 00aba742e4 Merge commit from fork
to avoid path traversal attacks
and remove the legacy allowance for fragments in MXCs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-12 09:08:00 +00:00
Andrew Ferrazzutti 35d862ebd3 Handle M_MAX_DELAY_EXCEEDED errors (#4511)
* Handle M_MAX_DELAY_EXCEEDED errors

Use a lower delay time if the server rejects a delay as too long.

* Add test

* Lint test

* Update src/matrixrtc/MatrixRTCSession.ts

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

* Test computed expiry timeout value

---------

Co-authored-by: Robin <robin@robin.town>
2024-11-11 20:48:53 +00:00
Hugh Nimmo-Smith 581b3209ab Allow configuration of MatrixRTC timers when calling joinRoomSession() (#4510) 2024-11-11 15:35:05 +00:00
Andrew Ferrazzutti 6855ace642 When state says you've left ongoing call, rejoin (#4342)
* When state says you've left ongoing call, rejoin

When receiving a state change that says you are no longer a member of a
RTC session that you are actually still participating in, send another
state event to put yourself back in the session membership.

This can happen when an administrator overwrites your call membership
event (which is allowed even with MSC3757's restrictions on state), or
if your delayed disconnection event (via MSC4140) timed out before your
client could send a heartbeat to delay it further.

* Don't emit state changed on join recovery
2024-11-11 15:07:33 +00:00
Florian Duros 5033d48013 Fix tsdoc error in rust-crypto folder (#4504) 2024-11-11 13:15:46 +00:00
Andrew Ferrazzutti 4c53836a13 Remove redundant type arguments in function call (#4507)
as the types can be deduced by the function arguments.
2024-11-11 11:19:48 +00:00
Andrew Ferrazzutti 10a4fd8328 MatrixRTCSession: handle rate limit errors (#4494)
* MatrixRTCSession: handle rate limit errors

* Lint

* Handle ratelimiting for non-legacy state setting

Each request must be retried, as the non-legacy flow involves a sequence
of requests that must resolve in order.

* Fix broken test

* Check for MSC3757 instead of the unmerged MSC3779

* Move helper out of beforeEach

* Test ratelimit errors
2024-11-11 02:55:42 +00:00
Andrew Ferrazzutti 98f7637683 Send/receive error details with widgets (#4492)
* Send/receive error details with widgets

* Fix embedded client tests

* Use all properties of error responses

* Lint

* Rewrite ternary expression as if statement

* Put typehints on overridden functions

* Lint

* Update matrix-widget-api

* Don't @link across packages

as gendoc fails when doing so.

* Add a missing docstring

* Set widget response error string to correct value

* Test conversion to/from widget error payloads

* Test processing errors thrown by widget transport

* Lint

* Test processing errors from transport.sendComplete
2024-11-09 07:29:04 +00:00
Florian Duros 0df8e81da4 Deprecate MatrixClient.getKeyBackupVersion (#4505) 2024-11-08 09:06:09 +00:00
Florian Duros 30b1894f37 Deprecate unused callback in CryptoCallbacks (#4501) 2024-11-06 15:38:15 +00:00
Richard van der Hoff fbbdb6e766 Remove dead release scripts (#4496)
* Remove redundant `pre-release.sh` script

This is now a no-op (there are no `matrix_lib` fields in package.json), so we
may as well remove it.

* Remove redundant `post-merge-master` script

Just as pre-release is a no-op, so is this

* Remove redundant switch_package_to_release script

Once more: this script is a no-op.
2024-11-05 16:38:13 +00:00
RiotRobot 5a1488ebd5 Merge branch 'master' into develop 2024-11-05 13:49:42 +00:00
David Baker 794f044dda Make doc clearer on getCrossSigningKeyId (#4477)
* Make doc clearer on getCrossSigningKeyId

I was trying to work out why this was being used in a check. It
turns out it only returns the key ID if the private part is stored
locally, which seems very much non-obvious.

* Better doc

* Formatting & clarity

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

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-11-04 10:53:30 +00:00
Michael Telatynski a197afe8aa Refactor MatrixClient::forget to not abuse membershipChange API (#4490)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-04 09:49:27 +00:00
Florian Duros 1061b93b29 Remove remaining legacy crypto imports in new crypto and tests (#4491)
* Use `CryptoCallbacks` from `CryptoApi` instead of legacy crypto.

* Use `KeyBackup` from rust crypto instead of `IKeyBackup` from legacy crypto
2024-11-04 08:55:32 +00:00
Hugh Nimmo-Smith 6528a59fc1 Reduce cognitive complexity of Room.addLiveEvents() (#4493) 2024-11-01 17:38:08 +00:00
Will Hunt f6a169b5a5 Replace usages of global with globalThis (#4489)
* Update src with globalThis

* Update spec with globalThis

* Replace in more spec/ places

* More changes to src/

* Add a linter rule for global

* Prettify

* lint
2024-11-01 09:15:21 +00:00
renovate[bot] d04135cc1c Update dependency uuid to v11 (#4482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 11:39:59 +00:00
Andrew Ferrazzutti 546047a050 Capture HTTP error response headers & handle Retry-After header (MSC4041) (#4471)
* Include HTTP response headers in MatrixError

* Lint

* Support MSC4041 / Retry-After header

* Fix tests

* Remove redundant MatrixError parameter properties

They are inherited from HTTPError, so there is no need to mark them as
parameter properties.

* Comment that retry_after_ms is deprecated

* Properly handle colons in XHR header values

Also remove the negation in the if-condition for better readability

* Improve Retry-After parsing and docstring

* Revert ternary operator to if statements

for readability

* Reuse resolved Headers for Content-Type parsing

* Treat empty Content-Type differently from null

* Add MatrixError#isRateLimitError

This is separate from MatrixError#getRetryAfterMs because it's possible
for a rate-limit error to have no Retry-After time, and having separate
methods to check each makes that more clear.

* Ignore HTTP status code when getting Retry-After

because status codes other than 429 may have Retry-After

* Catch Retry-After parsing errors

* Add test coverage for HTTP error headers

* Update license years

* Move safe Retry-After lookup to global function

so it can more conveniently check if an error is a MatrixError

* Lint

* Inline Retry-After header value parsing

as it is only used in one place and doesn't need to be exported

* Update docstrings

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

* Use bare catch

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

* Give HTTPError methods for rate-limit checks

and make MatrixError inherit them

* Cover undefined errcode in rate-limit check

* Update safeGetRetryAfterMs docstring

Be explicit that errors that don't look like rate-limiting errors will
not pull a retry delay value from the error.

* Use rate-limit helper functions in more places

* Group the header tests

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-10-30 15:52:34 +00:00
Florian Duros 16153e5d82 Replace legacy keyBackup types (#4486) 2024-10-30 13:12:27 +00:00
Hugh Nimmo-Smith fd73d5068c Add RoomWidgetClient.sendToDeviceViaWidgetApi() (#4475) 2024-10-30 09:36:44 +00:00
Richard van der Hoff e72859a44a Update codeowners for crypto-api (#4478)
Obviously, the crypto api is owned by the (web) crypto team!
2024-10-29 17:02:14 +00:00
169 changed files with 5346 additions and 2959 deletions
+5
View File
@@ -63,6 +63,10 @@ module.exports = {
name: "setImmediate",
message: "Use setTimeout instead.",
},
{
name: "global",
message: "Use globalThis instead.",
},
],
"import/no-restricted-paths": [
@@ -134,6 +138,7 @@ module.exports = {
tryExtensions: [".ts"],
},
],
"no-extra-boolean-cast": "error",
},
},
{
+1
View File
@@ -8,6 +8,7 @@
/spec/*/webrtc @matrix-org/element-call-reviewers
/spec/*/matrixrtc @matrix-org/element-call-reviewers
/src/crypto-api @matrix-org/element-crypto-web-reviewers
/src/crypto @matrix-org/element-crypto-web-reviewers
/src/rust-crypto @matrix-org/element-crypto-web-reviewers
/spec/integ/crypto @matrix-org/element-crypto-web-reviewers
+4 -4
View File
@@ -2,7 +2,7 @@
## Checklist
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@aac270e08f6b4547ada0b3800f88e1eb3ce9d400 # v1
uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+2
View File
@@ -7,6 +7,8 @@ on:
branches:
- develop
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
backport:
name: Backport
+5 -2
View File
@@ -5,16 +5,19 @@ on:
workflows: ["Static Analysis"]
types:
- completed
permissions: {}
jobs:
netlify:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-24.04
permissions:
actions: read
deployments: write
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: docs
path: docs
@@ -13,7 +13,7 @@ on:
#
#push:
# branches: [develop, master]
permissions: {} # No permissions required
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
+1
View File
@@ -3,6 +3,7 @@ on:
push:
branches: [develop]
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
notify-downstream:
# Only respect triggers from our develop branch, ignore that of forks
+3
View File
@@ -9,6 +9,7 @@ on:
ELEMENT_BOT_TOKEN:
required: true
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
changelog:
name: Preview Changelog
@@ -44,6 +45,8 @@ jobs:
name: Label Community PRs
runs-on: ubuntu-24.04
if: github.event.action == 'opened'
permissions:
pull-requests: write
steps:
- name: Check membership
if: github.event.pull_request.user.login != 'renovate[bot]'
+38
View File
@@ -0,0 +1,38 @@
name: Release Sanity checks
on:
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: false
inputs:
repository:
type: string
required: false
default: ${{ github.repository }}
description: "The repository (in form owner/repo) to check for release blockers"
permissions: {}
jobs:
checks:
name: Sanity checks
runs-on: ubuntu-24.04
steps:
- name: Check for X-Release-Blocker label on any open issues or PRs
uses: actions/github-script@v7
env:
REPO: ${{ inputs.repository }}
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const { REPO } = process.env;
const { data } = await github.rest.search.issuesAndPullRequests({
q: `repo:${REPO} label:X-Release-Blocker is:open`,
per_page: 50,
});
if (data.total_count) {
data.items.forEach(item => {
core.error(`Release blocker: ${item.html_url}`);
});
core.setFailed(`Found release blockers!`);
}
@@ -8,9 +8,12 @@ on:
type: string
required: false
concurrency: release-drafter-action
permissions: {}
jobs:
draft:
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
+3
View File
@@ -8,6 +8,9 @@ on:
branches: [staging]
workflow_dispatch: {}
concurrency: ${{ github.workflow }}
permissions: {}
jobs:
draft:
permissions:
contents: write
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
+2 -3
View File
@@ -13,12 +13,14 @@ on:
type: string
required: false
concurrency: ${{ github.workflow }}
permissions: {} # Uses ELEMENT_BOT_TOKEN
jobs:
merge:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
@@ -49,9 +51,6 @@ jobs:
git checkout develop
git merge -X ours master
- name: Run post-merge-master script to revert package.json fields
run: ./.action-repo/scripts/release/post-merge-master.sh
- name: Reset dependencies
if: inputs.dependencies
run: |
+22 -42
View File
@@ -20,10 +20,6 @@ on:
description: Publish to npm
type: boolean
default: false
downstreams:
description: List of github projects (owner/repo) which should have their dependency bumped to the newly released version (in JSON string array string syntax)
type: string
required: false
gpg-fingerprint:
description: Fingerprint of the GPG key to use for signing the git tag and assets, if any.
type: string
@@ -38,16 +34,31 @@ on:
description: The number of expected assets, including signatures, excluding generated zip & tarball.
type: number
required: false
outputs:
npm-id:
description: "The npm package@version string we published"
value: ${{ jobs.npm.outputs.id }}
permissions: {}
jobs:
checks:
name: Sanity checks
permissions:
issues: read
pull-requests: read
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
release:
name: Release
runs-on: ubuntu-24.04
environment: Release
needs: checks
permissions:
contents: write
steps:
- name: Load GPG key
id: gpg
if: inputs.gpg-fingerprint
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
@@ -65,6 +76,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: staging
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
@@ -136,7 +148,9 @@ jobs:
done
- name: Bump package.json version
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
run: |
yarn version --no-git-tag-version --new-version "${VERSION#v}"
git add package.json
- name: Add to CHANGELOG.md
if: inputs.final
@@ -158,11 +172,6 @@ jobs:
env:
RELEASE_NOTES: ${{ steps.draft-release.outputs.body }}
- name: Run pre-release script to update package.json fields
run: |
./.action-repo/scripts/release/pre-release.sh
git add package.json
- name: Commit changes
run: git commit -m "$VERSION"
@@ -277,6 +286,8 @@ jobs:
name: Post release steps
needs: release
runs-on: ubuntu-24.04
permissions:
issues: write
steps:
- id: repository
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
@@ -300,34 +311,3 @@ jobs:
# wait-interval: 10
# check-name: merge
# allowed-conclusions: success
bump-downstreams:
name: Update npm dependency in downstream projects
needs: npm
runs-on: ubuntu-24.04
if: inputs.downstreams
strategy:
matrix:
repo: ${{ fromJSON(inputs.downstreams) }}
steps:
- uses: actions/checkout@v4
with:
repository: ${{ matrix.repo }}
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Bump dependency
env:
DEPENDENCY: ${{ needs.npm.outputs.id }}
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact
git add package.json yarn.lock
git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging
+5
View File
@@ -8,6 +8,7 @@ on:
id:
description: "The npm package@version string we published"
value: ${{ jobs.npm.outputs.id }}
permissions: {} # No permissions required
jobs:
npm:
name: Publish to npm
@@ -39,6 +40,10 @@ jobs:
tag: next
ignore-scripts: false
- name: Check npm package was published
if: steps.npm-publish.outputs.id == ''
run: exit 1
- name: 🎖️ Add `latest` dist-tag to final releases
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
run: npm dist-tag add "$release" latest
+36 -1
View File
@@ -21,14 +21,49 @@ on:
type: boolean
default: true
concurrency: ${{ github.workflow }}
permissions: {} # No permissions required
jobs:
release:
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
permissions:
contents: write
issues: write
pull-requests: read
secrets: inherit
with:
final: ${{ inputs.mode == 'final' }}
npm: ${{ inputs.npm }}
downstreams: '["element-hq/element-web"]'
bump-downstreams:
name: Update npm dependency in downstream projects
needs: release
runs-on: ubuntu-24.04
strategy:
matrix:
repo:
- element-hq/element-web
steps:
- uses: actions/checkout@v4
with:
repository: ${{ matrix.repo }}
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Bump dependency
env:
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact
git add package.json yarn.lock
git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging
docs:
name: Publish Documentation
+18 -6
View File
@@ -5,23 +5,29 @@ on:
secrets:
SONAR_TOKEN:
required: true
# No longer used
ELEMENT_BOT_TOKEN:
required: true
required: false
inputs:
sharded:
type: boolean
required: false
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
permissions: {}
jobs:
sonarqube:
runs-on: ubuntu-24.04
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'merge_group'
permissions:
actions: read
statuses: write
id-token: write # sonar
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
- uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
@@ -30,7 +36,7 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
@@ -40,7 +46,7 @@ jobs:
uses: actions/download-artifact@v4
if: ${{ !inputs.sharded }}
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: coverage
path: coverage
@@ -48,11 +54,17 @@ jobs:
uses: actions/download-artifact@v4
if: inputs.sharded
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: coverage-*
path: coverage
merge-multiple: true
- name: Check coverage artifact
run: |
if [ ! -d coverage ]; then
echo "Coverage not found. Exiting with failure."
exit 1
fi
- id: extra_args
run: |
@@ -75,7 +87,7 @@ jobs:
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
- uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
+5
View File
@@ -7,10 +7,15 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
permissions: {}
jobs:
sonarqube:
name: 🩻 SonarQube
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
permissions:
actions: read
statuses: write
id-token: write # sonar
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+35 -10
View File
@@ -8,6 +8,7 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {} # No permissions needed
jobs:
ts_lint:
name: "Typescript Syntax Check"
@@ -26,16 +27,6 @@ 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-24.04
@@ -111,3 +102,37 @@ jobs:
- name: Run linter
run: "yarn run lint:knip"
element-web:
name: Downstream tsc element-web
if: github.event_name == 'merge_group'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
repository: element-hq/element-web
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Install Dependencies
run: "./scripts/layered.sh"
env:
# tell layered.sh to check out the right sha of the JS-SDK
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
- name: Typecheck
run: "yarn run lint:types"
# Hook for branch protection to skip downstream typechecking outside of merge queues
downstream:
name: Downstream Typescript Syntax Check
runs-on: ubuntu-24.04
if: always()
needs:
- element-web
steps:
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
run: exit 1
+1
View File
@@ -8,6 +8,7 @@ on:
- develop
paths:
- .github/labels.yml
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
sync-labels:
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
+7 -1
View File
@@ -10,6 +10,7 @@ concurrency:
cancel-in-progress: true
env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
permissions: {} # No permissions required
jobs:
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
@@ -77,6 +78,8 @@ jobs:
name: Downstream test element-web
if: github.event_name == 'merge_group'
uses: element-hq/element-web/.github/workflows/tests.yml@develop
permissions:
statuses: write
with:
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
@@ -84,6 +87,7 @@ jobs:
complement-crypto:
name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group'
permissions: read-all
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
with:
use_js_sdk: "."
@@ -107,10 +111,12 @@ jobs:
if: always()
needs:
- element-web
permissions:
statuses: write
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
+1 -1
View File
@@ -3,7 +3,7 @@ name: Move new issues into Issue triage board
on:
issues:
types: [opened]
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
automate-project-columns-next:
runs-on: ubuntu-24.04
+1 -1
View File
@@ -3,7 +3,7 @@ name: Move labelled issues to correct projects
on:
issues:
types: [labeled]
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
call-triage-labelled:
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
+88
View File
@@ -1,3 +1,91 @@
Changes in [35.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.1.0) (2024-12-18)
==================================================================================================
This release updates matrix-sdk-crypto-wasm to fix a bug which could prevent loading stored crypto state from storage.
## 🐛 Bug Fixes
* Upgrade matrix-sdk-crypto-wasm to 1.11.0 ([#4593](https://github.com/matrix-org/matrix-js-sdk/pull/4593)).
Changes in [35.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.0.0) (2024-12-17)
==================================================================================================
## 🚨 BREAKING CHANGES
This release contains several breaking changes which will need code changes in your app. Most notably, `initCrypto()`
no longer exists and has been moved to `initLegacyCrypto()` in preparation for the eventual removal of Olm. You can
continue to use legacy Olm crypto for now by calling `initLegacyCrypto()` instead.
You may also need to make further changes if you use more advanced APIs. See the individual PRs (listed in order of size of change) for specific APIs changed and how to migrate.
* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros.
* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr.
* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy.
## 🦖 Deprecations
* Deprecate remaining legacy functions and move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler ([#4560](https://github.com/matrix-org/matrix-js-sdk/pull/4560)). Contributed by @florianduros.
## ✨ Features
* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros.
* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy.
* Re-send MatrixRTC media encryption keys for a new joiner even if a rotation is in progress ([#4561](https://github.com/matrix-org/matrix-js-sdk/pull/4561)). Contributed by @hughns.
* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr.
* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)" ([#4532](https://github.com/matrix-org/matrix-js-sdk/pull/4532)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix age field check in event echo processing ([#3635](https://github.com/matrix-org/matrix-js-sdk/pull/3635)). Contributed by @stas-demydiuk.
Changes in [34.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.13.0) (2024-12-03)
====================================================================================================
## 🦖 Deprecations
* Deprecate `MatrixClient.isEventSenderVerified` ([#4527](https://github.com/matrix-org/matrix-js-sdk/pull/4527)). Contributed by @florianduros.
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
## ✨ Features
* Ensure we disambiguate display names which look like MXIDs ([#4540](https://github.com/matrix-org/matrix-js-sdk/pull/4540)). Contributed by @t3chguy.
* Add `CryptoApi.getBackupInfo` ([#4512](https://github.com/matrix-org/matrix-js-sdk/pull/4512)). Contributed by @florianduros.
* Fix local echo in embedded mode ([#4498](https://github.com/matrix-org/matrix-js-sdk/pull/4498)). Contributed by @toger5.
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Fix `RustBackupManager` remaining values after current backup removal ([#4537](https://github.com/matrix-org/matrix-js-sdk/pull/4537)). Contributed by @florianduros.
Changes in [34.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.12.0) (2024-11-19)
====================================================================================================
## 🦖 Deprecations
* Deprecate `MatrixClient.getKeyBackupVersion` ([#4505](https://github.com/matrix-org/matrix-js-sdk/pull/4505)). Contributed by @florianduros.
* Deprecate unused callbacks in `CryptoCallbacks` ([#4501](https://github.com/matrix-org/matrix-js-sdk/pull/4501)). Contributed by @florianduros.
## ✨ Features
* Handle M\_MAX\_DELAY\_EXCEEDED errors ([#4511](https://github.com/matrix-org/matrix-js-sdk/pull/4511)). Contributed by @AndrewFerr.
* Allow configuration of MatrixRTC timers when calling joinRoomSession() ([#4510](https://github.com/matrix-org/matrix-js-sdk/pull/4510)). Contributed by @hughns.
* When state says you've left ongoing call, rejoin ([#4342](https://github.com/matrix-org/matrix-js-sdk/pull/4342)). Contributed by @AndrewFerr.
* Remove redundant type arguments in function call ([#4507](https://github.com/matrix-org/matrix-js-sdk/pull/4507)). Contributed by @AndrewFerr.
* MatrixRTCSession: handle rate limit errors ([#4494](https://github.com/matrix-org/matrix-js-sdk/pull/4494)). Contributed by @AndrewFerr.
* Send/receive error details with widgets ([#4492](https://github.com/matrix-org/matrix-js-sdk/pull/4492)). Contributed by @AndrewFerr.
* Capture HTTP error response headers \& handle Retry-After header (MSC4041) ([#4471](https://github.com/matrix-org/matrix-js-sdk/pull/4471)). Contributed by @AndrewFerr.
* Add RoomWidgetClient.sendToDeviceViaWidgetApi() ([#4475](https://github.com/matrix-org/matrix-js-sdk/pull/4475)). Contributed by @hughns.
Changes in [34.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.1) (2024-11-12)
====================================================================================================
# Security
- Fixes for [CVE-2024-50336](https://nvd.nist.gov/vuln/detail/CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr).
Changes in [34.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.0) (2024-11-12)
====================================================================================================
# Security
- Fixes for [CVE-2024-50336](https://nvd.nist.gov/vuln/detail/CVE-2024-50336) / [GHSA-xvg8-m4x3-w6xr](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-xvg8-m4x3-w6xr).
Changes in [34.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.10.0) (2024-11-05)
====================================================================================================
## 🦖 Deprecations
+136 -49
View File
@@ -38,8 +38,8 @@ client.publicRooms(function (err, data) {
});
```
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node/README.md) for a more complex example.
See [below](#end-to-end-encryption-support) for how to enable end-to-end-encryption, or check
[the Node.js terminal app](https://github.com/matrix-org/matrix-js-sdk/tree/develop/examples/node) for a more complex example.
To start the client:
@@ -126,26 +126,26 @@ const img = await fetch(downloadUrl, {
This SDK provides a full object model around the Matrix Client-Server API and emits
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
- Handles syncing (via `/sync`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
- Handles syncing (via `/sync`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
# Usage
@@ -303,44 +303,131 @@ Then visit `http://localhost:8005` to see the API docs.
# End-to-end encryption support
**This section is outdated.** Use of `libolm` is deprecated and we are replacing it with support
from the matrix-rust-sdk (https://github.com/element-hq/element-web/issues/21972).
`matrix-js-sdk`'s end-to-end encryption support is based on the [WebAssembly bindings](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm) of the Rust [matrix-sdk-crypto](https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-crypto) library.
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
application to make libolm available, via the `Olm` global.
## Initialization
It is also necessary to call `await matrixClient.initCrypto()` after creating a new
`MatrixClient` (but **before** calling `matrixClient.startClient()`) to
initialise the crypto layer.
**Do not use `matrixClient.initLegacyCrypto()`. This method is deprecated and no longer maintained.**
If the `Olm` global is not available, the SDK will show a warning, as shown
below; `initCrypto()` will also fail.
To initialize the end-to-end encryption support in the matrix client:
```
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
```javascript
// Create a new matrix client
const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId,
});
// Initialize to enable end-to-end encryption support.
await matrixClient.initRustCrypto();
```
If the crypto layer is not (successfully) initialised, the SDK will continue to
work for unencrypted rooms, but it will not support the E2E parts of the Matrix
specification.
After calling `initRustCrypto`, you can obtain a reference to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html) interface, which is the main entry point for end-to-end encryption, by calling [`MatrixClient.getCrypto`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#getCrypto).
To provide the Olm library in a browser application:
**WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for ensuring that only one `MatrixClient` issue is instantiated at a time.
- download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
- load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
## Secret storage
To provide the Olm library in a node.js application:
You should normally set up [secret storage](https://spec.matrix.org/v1.12/client-server-api/#secret-storage) before using the end-to-end encryption. To do this, call [`CryptoApi.bootstrapSecretStorage`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapSecretStorage).
`bootstrapSecretStorage` can be called unconditionally: it will only set up the secret storage if it is not already set up (unless you use the `setupNewSecretStorage` parameter).
- `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`
(replace the URL with the latest version you want to use from
https://packages.matrix.org/npm/olm/)
- `global.Olm = require('olm');` _before_ loading `matrix-js-sdk`.
```javascript
const matrixClient = sdk.createClient({
...,
cryptoCallbacks: {
getSecretStorageKey: async (keys) => {
// This function should prompt the user to enter their secret storage key.
return mySecretStorageKeys;
},
},
});
If you want to package Olm as dependency for your node.js application, you can
use `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`. If your
application also works without e2e crypto enabled, add `--optional` to mark it
as an optional dependency.
matrixClient.getCrypto().bootstrapSecretStorage({
// This function will be called if a new secret storage key (aka recovery key) is needed.
// You should prompt the user to save the key somewhere, because they will need it to unlock secret storage in future.
createSecretStorageKey: async () => {
return mySecretStorageKey;
},
});
```
The example above will create a new secret storage key if secret storage was not previously set up.
The secret storage data will be encrypted using the secret storage key returned in [`createSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CreateSecretStorageOpts.html#createSecretStorageKey).
We recommend that you prompt the user to re-enter this key when [`CryptoCallbacks.getSecretStorageKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) is called (when the secret storage access is needed).
## Set up cross-signing
To set up cross-signing to verify devices and other users, call
[`CryptoApi.bootstrapCrossSigning`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapCrossSigning):
```javascript
matrixClient.getCrypto().bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (makeRequest) => {
return makeRequest(authDict);
},
});
```
The [`authUploadDeviceSigningKeys`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.BootstrapCrossSigningOpts.html#authUploadDeviceSigningKeys)
callback is required in order to upload newly-generated public cross-signing keys to the server.
## Key backup
If the user doesn't already have a [key backup](https://spec.matrix.org/v1.12/client-server-api/#server-side-key-backups) you should create one:
```javascript
// Check if we have a key backup.
// If checkKeyBackupAndEnable returns null, there is no key backup.
const hasKeyBackup = (await matrixClient.getCrypto().checkKeyBackupAndEnable()) !== null;
// Create the key backup
await matrixClient.getCrypto().resetKeyBackup();
```
## Verify a new device
Once the cross-signing is set up on one of your devices, you can verify another device with two methods:
1. Use `CryptoApi.bootstrapCrossSigning`.
`bootstrapCrossSigning` will call the [CryptoCallbacks.getSecretStorageKey](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoCallbacks.html#getSecretStorageKey) callback. The device is verified with the private cross-signing keys fetched from the secret storage.
2. Request an interactive verification against existing devices, by calling [CryptoApi.requestOwnUserVerification](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#requestOwnUserVerification).
## Migrating from the legacy crypto stack to Rust crypto
If your application previously used the legacy crypto stack, (i.e, it called `MatrixClient.initLegacyCrypto()`), you will
need to migrate existing devices to the Rust crypto stack.
This migration happens automatically when you call `initRustCrypto()` instead of `initLegacyCrypto()`,
but you need to provide the legacy [`cryptoStore`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#cryptoStore) and [`pickleKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#pickleKey) to [`createClient`](https://matrix-org.github.io/matrix-js-sdk/functions/matrix.createClient.html):
```javascript
// You should provide the legacy crypto store and the pickle key to the matrix client in order to migrate the data.
const matrixClient = sdk.createClient({
cryptoStore: myCryptoStore,
pickleKey: myPickleKey,
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId,
});
// The migration will be done automatically when you call `initRustCrypto`.
await matrixClient.initRustCrypto();
```
To follow the migration progress, you can listen to the [`CryptoEvent.LegacyCryptoStoreMigrationProgress`](https://matrix-org.github.io/matrix-js-sdk/enums/crypto_api.CryptoEvent.html#LegacyCryptoStoreMigrationProgress) event:
```javascript
// When progress === total === -1, the migration is finished.
matrixClient.on(CryptoEvent.LegacyCryptoStoreMigrationProgress, (progress, total) => {
...
});
```
The Rust crypto stack is not supported in a lot of deprecated methods of [`MatrixClient`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html). If you use them, you should migrate to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html). Also, the legacy `MatrixClient.crypto` object is not available any more: you should use `MatrixClient.getCrypto()` instead.
# Contributing
+3 -3
View File
@@ -1,8 +1,8 @@
# Summary
- [Introduction](../README.md)
- [Introduction](../README.md)
# Deep dive
- [Storage notes](storage-notes.md)
- [Unverified devices](warning-on-unverified-devices.md)
- [Storage notes](storage-notes.md)
- [Unverified devices](warning-on-unverified-devices.md)
+27 -27
View File
@@ -20,19 +20,19 @@ blurrier.
When we are low on disk space overall or near the group limit / origin quota:
- Chrome
- Log database may fail to start with AbortError
- IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
- When near the quota, QuotaExceededError is used more consistently
- Firefox
- The first error will be QuotaExceededError
- Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
- Once you start getting errors, the DB is effectively wedged in read-only
mode
- Can revive access if you reopen the DB
- Chrome
- Log database may fail to start with AbortError
- IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
- When near the quota, QuotaExceededError is used more consistently
- Firefox
- The first error will be QuotaExceededError
- Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
- Once you start getting errors, the DB is effectively wedged in read-only
mode
- Can revive access if you reopen the DB
## Cache Eviction
@@ -41,9 +41,9 @@ limited by a single quota, in practice, browsers appear to handle `localStorage`
separately from the others, so it has a separate quota limit and isn't evicted
when low on space.
- Chrome, Firefox
- IndexedDB for origin deleted
- Local Storage remains in place
- Chrome, Firefox
- IndexedDB for origin deleted
- Local Storage remains in place
## Persistent Storage
@@ -51,20 +51,20 @@ Storage Standard offers a `navigator.storage.persist` API that can be used to
request persistent storage that won't be deleted by the browser because of low
space.
- Chrome
- Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
- Firefox
- Firefox 67 shows a prompt to grant
- Reverting persistent seems to require revoking permission _and_ clearing
site data
- Chrome
- Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
- Firefox
- Firefox 67 shows a prompt to grant
- Reverting persistent seems to require revoking permission _and_ clearing
site data
## Storage Estimation
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
how much space remains.
- Chrome, Firefox
- Can run this at any time to request an estimate of space remaining
- Firefox
- Returns `0` for `usage` if a site is persisted
- Chrome, Firefox
- Can run this at any time to request an estimate of space remaining
- Firefox
- Returns `0` for `usage` if a site is persisted
+7 -7
View File
@@ -17,13 +17,13 @@ Warn when you initial sync if the room has any new undefined devices since you w
Warn when the user tries to send a message:
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
...this should be good enough.
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
...this should be good enough.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
throw an error when trying to encrypt if there are pure unverified devices there
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
- or megolm could warn which devices are causing the problems.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
throw an error when trying to encrypt if there are pure unverified devices there
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
- or megolm could warn which devices are causing the problems.
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "34.10.0",
"version": "35.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
@@ -50,7 +50,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^11.1.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
@@ -58,12 +58,12 @@
"jwt-decode": "^4.0.0",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.8.2",
"matrix-widget-api": "^1.10.0",
"oidc-client-ts": "^3.0.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
"uuid": "10"
"uuid": "11"
},
"devDependencies": {
"@action-validator/cli": "^0.6.0",
@@ -103,7 +103,7 @@
"eslint-plugin-jsdoc": "^50.0.0",
"eslint-plugin-matrix-org": "^2.0.1",
"eslint-plugin-n": "^14.0.0",
"eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-unicorn": "^56.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "11.1.5",
@@ -117,12 +117,12 @@
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.3.3",
"prettier": "3.4.1",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"typedoc": "^0.26.0",
"typedoc": "^0.27.0",
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-mdn-links": "^4.0.0",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.4.2"
},
-22
View File
@@ -1,22 +0,0 @@
#!/bin/bash
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
for i in main typings browser
do
# If a `lib` prefixed value is present, it means we adjusted the field earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
# For the published and dist versions of the package,
# we copy the `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if they exist).
# This small bit of gymnastics allows us to use the TypeScript source directly for development without
# needing to build before linting or testing.
for i in main typings browser
do
lib_value=$(jq -r ".matrix_lib_$i" package.json)
if [ "$lib_value" != "null" ]; then
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
done
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env node
const fsProm = require("fs/promises");
const PKGJSON = "package.json";
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
for (const field of ["main", "typings"]) {
if (pkgJson["matrix_lib_" + field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_" + field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
-1
View File
@@ -1 +0,0 @@
switch_package_to_release.cjs
+1 -1
View File
@@ -66,7 +66,7 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
fetchFn: this.httpBackend.fetchFn as typeof globalThis.fetch,
...options,
};
if (!fullOptions.cryptoStore) {
+15 -15
View File
@@ -91,7 +91,6 @@ import {
OnlySignedDevicesIsolationMode,
} from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { IKeyBackup } from "../../../src/crypto/backup";
import {
createOlmAccount,
createOlmSession,
@@ -106,6 +105,7 @@ import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
import { KnownMembership } from "../../../src/@types/membership";
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -136,7 +136,7 @@ async function expectSendRoomKey(
recipientOlmAccount: Olm.Account,
recipientOlmSession: Olm.Session | null = null,
): Promise<Olm.InboundGroupSession> {
const Olm = global.Olm;
const Olm = globalThis.Olm;
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
function onSendRoomKey(content: any): Olm.InboundGroupSession {
@@ -219,7 +219,7 @@ async function expectSendMegolmMessage(
}
describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, initCrypto: InitCrypto) => {
if (!global.Olm) {
if (!globalThis.Olm) {
// currently we use libolm to implement the crypto in the tests, so need it to be present.
logger.warn("not running megolm tests: Olm not present");
return;
@@ -230,7 +230,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend !== "rust-sdk" ? test.skip : test;
const Olm = global.Olm;
const Olm = globalThis.Olm;
let testOlmAccount = {} as unknown as Olm.Account;
let testSenderKey = "";
@@ -1327,7 +1327,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const syncResponse = getSyncResponse(["@bob:xyz"]);
// Every 2 messages in the room, the session should be rotated
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = {
syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
algorithm: "m.megolm.v1.aes-sha2",
rotation_period_msgs: 2,
};
@@ -1383,7 +1383,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const oneHourInMs = 60 * 60 * 1000;
// Every 1h the session should be rotated
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = {
syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
algorithm: "m.megolm.v1.aes-sha2",
rotation_period_ms: oneHourInMs,
};
@@ -1741,7 +1741,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
groupSession: groupSession,
room_id: ROOM_ID,
});
await testClient.client.initCrypto();
await testClient.client.initLegacyCrypto();
const keys = [
{
room_id: ROOM_ID,
@@ -1853,7 +1853,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
oldBackendOnly("Alice receives shared history before being invited to a room by the sharer", async () => {
const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux");
await beccaTestClient.client.initCrypto();
await beccaTestClient.client.initLegacyCrypto();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -1897,13 +1897,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
const p2pSession = new globalThis.Olm.Session();
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
"readonly",
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
const account = new globalThis.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key);
@@ -2007,7 +2007,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
oldBackendOnly("Alice receives shared history before being invited to a room by someone else", async () => {
const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux");
await beccaTestClient.client.initCrypto();
await beccaTestClient.client.initLegacyCrypto();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -2045,13 +2045,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
const p2pSession = new globalThis.Olm.Session();
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
"readonly",
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
const account = new global.Olm.Account();
const account = new globalThis.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key);
@@ -3138,11 +3138,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// Import a new key that should be uploaded
const newKey = testData.MEGOLM_SESSION_DATA;
const awaitKeyUploaded = new Promise<IKeyBackup>((resolve) => {
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
resolve(uploadPayload);
return {
status: 200,
+160 -48
View File
@@ -22,7 +22,7 @@ import { Mocked } from "jest-mock";
import {
createClient,
Crypto,
CryptoEvent,
encodeBase64,
ICreateClientOpts,
IEvent,
IMegolmSessionData,
@@ -42,10 +42,10 @@ import {
} from "../../test-utils/test-utils";
import * as testData from "../../test-utils/test-data";
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
import { IKeyBackup } from "../../../src/crypto/backup";
import { flushPromises } from "../../test-utils/flushPromises";
import { defer, IDeferred } from "../../../src/utils";
import { DecryptionFailureCode } from "../../../src/crypto-api";
import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent } from "../../../src/crypto-api";
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
const ROOM_ID = testData.TEST_ROOM_ID;
@@ -91,7 +91,7 @@ function mockUploadEmitter(
},
};
}
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) {
@@ -117,8 +117,10 @@ function mockUploadEmitter(
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
// const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
// const newBackendOnly = backend === "libolm" ? test.skip : test;
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
const newBackendOnly = backend === "libolm" ? test.skip : test;
const isNewBackend = backend === "rust-sdk";
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests on the test homeserver */
@@ -247,9 +249,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// On the first decryption attempt, decryption fails.
await awaitDecryption(event);
expect(event.decryptionFailureReason).toEqual(
backend === "libolm"
? DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID
: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
isNewBackend
? DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP
: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);
// Eventually, decryption succeeds.
@@ -314,6 +316,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
fetchMock.get(
`path:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`,
testData.SIGNED_BACKUP_DATA,
);
aliceClient = await initTestClient();
aliceCrypto = aliceClient.getCrypto()!;
@@ -344,20 +350,29 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
onKeyCached = resolve;
});
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);
const result = await advanceTimersUntil(
aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
isNewBackend
? aliceCrypto.restoreKeyBackup()
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
);
expect(result.imported).toStrictEqual(1);
if (isNewBackend) return;
await awaitKeyCached;
// The key should be now cached
@@ -398,8 +413,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
it("Should import full backup in chunks", async function () {
const importMockImpl = jest.fn();
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = importMockImpl;
if (isNewBackend) {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
} else {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
}
// We need several rooms with several sessions to test chunking
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
@@ -408,17 +428,26 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const check = await aliceCrypto.checkKeyBackupAndEnable();
const progressCallback = jest.fn();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup({
progressCallback,
})
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
));
expect(result.imported).toStrictEqual(expectedTotal);
// Should be called 5 times: 200*4 plus one chunk with the remaining 32
expect(importMockImpl).toHaveBeenCalledTimes(5);
@@ -451,8 +480,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
it("Should continue to process backup if a chunk import fails and report failures", async function () {
// @ts-ignore - mock a private method for testing purpose
aliceCrypto.importBackedUpRoomKeys = jest
const importMockImpl = jest
.fn()
.mockImplementationOnce(() => {
// Fail to import first chunk
@@ -461,22 +489,36 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
// Ok for other chunks
.mockResolvedValue(undefined);
if (isNewBackend) {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
} else {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
}
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
const check = await aliceCrypto.checkKeyBackupAndEnable();
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);
const progressCallback = jest.fn();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
);
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup({ progressCallback })
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
progressCallback,
},
));
expect(result.total).toStrictEqual(expectedTotal);
// A chunk failed to import
@@ -527,20 +569,26 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", response);
const check = await aliceCrypto.checkKeyBackupAndEnable();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
await aliceCrypto.storeSessionBackupPrivateKey(
decodeRecoveryKey(testData.BACKUP_DECRYPTION_KEY_BASE58),
check!.backupInfo!.version!,
);
const result = await (isNewBackend
? aliceCrypto.restoreKeyBackup()
: aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
));
expect(result.total).toStrictEqual(expectedTotal);
// A chunk failed to import
expect(result.imported).toStrictEqual(expectedTotal - decryptionFailureCount);
});
it("recover specific session from backup", async function () {
oldBackendOnly("recover specific session from backup", async function () {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
@@ -560,7 +608,33 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(result.imported).toStrictEqual(1);
});
it("Fails on bad recovery key", async function () {
newBackendOnly(
"Should get the decryption key from the secret storage and restore the key backup",
async function () {
// @ts-ignore - mock a private method for testing purpose
jest.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64);
const fullBackup = {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
const decryptionKey = await aliceCrypto.getSessionBackupPrivateKey();
expect(encodeBase64(decryptionKey!)).toStrictEqual(testData.BACKUP_DECRYPTION_KEY_BASE64);
const result = await aliceCrypto.restoreKeyBackup();
expect(result.imported).toStrictEqual(1);
},
);
oldBackendOnly("Fails on bad recovery key", async function () {
const fullBackup = {
rooms: {
[ROOM_ID]: {
@@ -584,6 +658,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
),
).rejects.toThrow();
});
newBackendOnly("Should throw an error if the decryption key is not found in cache", async () => {
await expect(aliceCrypto.restoreKeyBackup()).rejects.toThrow("No decryption key found in crypto store");
});
});
describe("backupLoop", () => {
@@ -890,6 +968,40 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
});
newBackendOnly("getKeyBackupInfo() should not return a backup if the active backup has been deleted", async () => {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
fetchMock.delete(`express:/_matrix/client/v3/room_keys/version/${testData.SIGNED_BACKUP_DATA.version}`, {});
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceCrypto.checkKeyBackupAndEnable();
// At this point there is no backup
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
// Return now the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
// Delete the backup and we are expecting the key backup to be disabled
const keyBackupStatus = defer<boolean>();
aliceClient.once(CryptoEvent.KeyBackupStatus, (enabled) => keyBackupStatus.resolve(enabled));
await aliceCrypto.deleteKeyBackupVersion(testData.SIGNED_BACKUP_DATA.version!);
expect(await keyBackupStatus.promise).toBe(false);
// The backup info should not be available anymore
expect(await aliceCrypto.getKeyBackupInfo()).toBeNull();
});
describe("isKeyBackupTrusted", () => {
it("does not trust a backup signed by an untrusted device", async () => {
aliceClient = await initTestClient();
+2 -2
View File
@@ -345,10 +345,10 @@ describe("MatrixClient crypto", () => {
beforeEach(async () => {
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
await aliTestClient.client.initLegacyCrypto();
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
await bobTestClient.client.initCrypto();
await bobTestClient.client.initLegacyCrypto();
aliMessages = [];
bobMessages = [];
+4 -4
View File
@@ -85,15 +85,15 @@ export function bootstrapCrossSigningTestOlmAccount(
deviceId: string,
keyBackupInfo: KeyBackupInfo[] = [],
): Partial<IDownloadKeyResult> {
const olmAliceMSK = new global.Olm.PkSigning();
const olmAliceMSK = new globalThis.Olm.PkSigning();
const masterPrivkey = olmAliceMSK.generate_seed();
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
const olmAliceUSK = new global.Olm.PkSigning();
const olmAliceUSK = new globalThis.Olm.PkSigning();
const userPrivkey = olmAliceUSK.generate_seed();
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
const olmAliceSSK = new global.Olm.PkSigning();
const olmAliceSSK = new globalThis.Olm.PkSigning();
const sskPrivkey = olmAliceSSK.generate_seed();
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
@@ -181,7 +181,7 @@ export async function createOlmSession(
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
const session = new globalThis.Olm.Session();
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
return session;
}
+33 -18
View File
@@ -78,6 +78,7 @@ import {
encryptGroupSessionKey,
encryptMegolmEvent,
encryptSecretSend,
getTestOlmAccountKeys,
ToDeviceEvent,
} from "./olm-utils";
import { KeyBackupInfo } from "../../../src/crypto-api";
@@ -90,7 +91,7 @@ jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
beforeAll(async () => {
// we use the libolm primitives in the test, so init the Olm library
await global.Olm.init();
await globalThis.Olm.init();
});
// load the rust library. This can take a few seconds on a slow GH worker.
@@ -264,7 +265,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
const olmSAS = new global.Olm.SAS();
const olmSAS = new globalThis.Olm.SAS();
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
// alice responds with a 'key' ...
@@ -358,7 +359,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept'
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
const olmSAS = new global.Olm.SAS();
const olmSAS = new globalThis.Olm.SAS();
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
@@ -473,21 +474,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request.phase).toEqual(VerificationPhase.Ready);
// we should now have QR data we can display
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
const rawQrCodeBuffer = (await request.generateQRCode())!;
expect(rawQrCodeBuffer).toBeTruthy();
const qrCodeBuffer = new Uint8Array(rawQrCodeBuffer);
const textDecoder = new TextDecoder();
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX");
expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version
expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode
const txnIdLen = qrCodeBuffer.readUint16BE(8);
expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId);
expect(textDecoder.decode(qrCodeBuffer.slice(0, 6))).toEqual("MATRIX");
expect(qrCodeBuffer[6]).toEqual(0x02); // version
expect(qrCodeBuffer[7]).toEqual(0x02); // mode
const txnIdLen = (qrCodeBuffer[8] << 8) + qrCodeBuffer[9];
expect(textDecoder.decode(qrCodeBuffer.slice(10, 10 + txnIdLen))).toEqual(transactionId);
// Alice's device's public key comes next, but we have nothing to do with it here.
// const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual(
Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"),
// const aliceDevicePubKey = qrCodeBuffer.slice(10 + txnIdLen, 32 + 10 + txnIdLen);
expect(encodeUnpaddedBase64(qrCodeBuffer.slice(42 + txnIdLen, 32 + 42 + txnIdLen))).toEqual(
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
);
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
const sharedSecret = qrCodeBuffer.slice(74 + txnIdLen);
// we should still be "Ready" and have no verifier
expect(request.phase).toEqual(VerificationPhase.Ready);
@@ -805,7 +808,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// we should now have QR data we can display
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
const sharedSecret = qrCodeBuffer.subarray(74 + transactionId.length);
const sharedSecret = qrCodeBuffer.slice(74 + transactionId.length);
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
@@ -990,6 +993,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
aliceClient.setGlobalErrorOnUnknownDevices(false);
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
await syncPromise(aliceClient);
// Rust crypto requires the sender's device keys before it accepts a
// verification request.
if (backend === "rust-sdk") {
const crypto = aliceClient.getCrypto()!;
const bobDeviceKeys = getTestOlmAccountKeys(testOlmAccount, BOB_TEST_USER_ID, "BobDevice");
e2eKeyResponder.addDeviceKeys(bobDeviceKeys);
syncResponder.sendOrQueueSyncResponse({ device_lists: { changed: [BOB_TEST_USER_ID] } });
await syncPromise(aliceClient);
await crypto.getUserDeviceInfo([BOB_TEST_USER_ID]);
}
});
/**
@@ -1627,7 +1642,7 @@ function buildReadyMessage(
}
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) {
function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayBuffer) {
return {
type: "m.key.verification.start",
content: {
@@ -1723,7 +1738,7 @@ function buildQRCode(
key2Base64: string,
sharedSecret: string,
mode = 0x02,
): Uint8Array {
): Uint8ClampedArray {
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
const qrCodeBuffer = Buffer.alloc(150); // oversize
@@ -1739,5 +1754,5 @@ function buildQRCode(
idx += qrCodeBuffer.write(sharedSecret, idx);
// truncate to the right length
return qrCodeBuffer.subarray(0, idx);
return new Uint8ClampedArray(qrCodeBuffer.subarray(0, idx));
}
+2 -2
View File
@@ -67,7 +67,7 @@ function getSyncResponse(roomMembers: string[]) {
}
describe("DeviceList management:", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("not running deviceList tests: Olm not present");
return;
}
@@ -77,7 +77,7 @@ describe("DeviceList management:", function () {
async function createTestClient() {
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
await testClient.client.initCrypto();
await testClient.client.initLegacyCrypto();
return testClient;
}
@@ -1144,7 +1144,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog
await room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
httpBackend
.when(
"GET",
@@ -1155,7 +1155,7 @@ describe("MatrixClient event timelines", function () {
});
await flushHttp(prom);
// but while loading the metadata, a new reply has arrived
await room.addLiveEvents([THREAD_REPLY3]);
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([
@@ -1247,7 +1247,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog
await room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
httpBackend
.when(
"GET",
@@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () {
});
await flushHttp(prom);
// but while loading the metadata, a new reply has arrived
await room.addLiveEvents([THREAD_REPLY3]);
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([
@@ -1560,7 +1560,7 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.NewReply);
respondToEvent(THREAD_ROOT_UPDATED);
await room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
await httpBackend.flushAllExpected();
await prom;
expect(thread.length).toBe(2);
@@ -1685,7 +1685,7 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.Update);
respondToEvent(THREAD_ROOT_UPDATED);
await room.addLiveEvents([THREAD_REPLY_REACTION]);
await room.addLiveEvents([THREAD_REPLY_REACTION], { addToState: false });
await httpBackend.flushAllExpected();
await prom;
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread
+57 -26
View File
@@ -168,14 +168,17 @@ describe("MatrixClient", function () {
type: "test",
content: {},
});
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
]);
room.addLiveEvents(
[
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
],
{ addToState: true },
);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
@@ -188,14 +191,17 @@ describe("MatrixClient", function () {
const roomId = "!roomId:server";
const roomAlias = "#my-fancy-room:server";
const room = new Room(roomId, client, userId);
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
]);
room.addLiveEvents(
[
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
],
{ addToState: true },
);
store.storeRoom(room);
// The method makes a request to resolve the alias
@@ -275,14 +281,17 @@ describe("MatrixClient", function () {
content: {},
});
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Knock,
event: true,
}),
]);
room.addLiveEvents(
[
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Knock,
event: true,
}),
],
{ addToState: true },
);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
@@ -641,9 +650,9 @@ describe("MatrixClient", function () {
}
beforeEach(function () {
// running initCrypto should trigger a key upload
// running initLegacyCrypto should trigger a key upload
httpBackend.when("POST", "/keys/upload").respond(200, {});
return Promise.all([client.initCrypto(), httpBackend.flush("/keys/upload", 1)]);
return Promise.all([client.initLegacyCrypto(), httpBackend.flush("/keys/upload", 1)]);
});
afterEach(() => {
@@ -1906,6 +1915,28 @@ describe("MatrixClient", function () {
return prom;
});
});
describe("getDomain", () => {
it("should return null if no userId is set", () => {
const client = new MatrixClient({ baseUrl: "http://localhost" });
expect(client.getDomain()).toBeNull();
});
it("should return the domain of the userId", () => {
expect(client.getDomain()).toBe("localhost");
});
});
describe("getUserIdLocalpart", () => {
it("should return null if no userId is set", () => {
const client = new MatrixClient({ baseUrl: "http://localhost" });
expect(client.getUserIdLocalpart()).toBeNull();
});
it("should return the localpart of the userId", () => {
expect(client.getUserIdLocalpart()).toBe("alice");
});
});
});
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
+2 -2
View File
@@ -80,7 +80,7 @@ describe("MatrixClient opts", function () {
let client: MatrixClient;
beforeEach(function () {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store: undefined,
baseUrl: baseUrl,
userId: userId,
@@ -135,7 +135,7 @@ describe("MatrixClient opts", function () {
let client: MatrixClient;
beforeEach(function () {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store: new MemoryStore() as IStore,
baseUrl: baseUrl,
userId: userId,
+170 -11
View File
@@ -112,7 +112,7 @@ describe("MatrixClient syncing", () => {
});
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
await client!.initCrypto();
await client!.initLegacyCrypto();
const roomId = "!cycles:example.org";
@@ -227,7 +227,7 @@ describe("MatrixClient syncing", () => {
});
it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
await client!.initCrypto();
await client!.initLegacyCrypto();
const roomId = "!cycles:example.org";
@@ -556,7 +556,7 @@ describe("MatrixClient syncing", () => {
});
it("should resolve incoming invites from /sync", () => {
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -577,7 +577,7 @@ describe("MatrixClient syncing", () => {
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => {
const member = client!.getRoom(roomOne)!.getMember(userC)!;
expect(member.name).toEqual("The Boss");
expect(member.getAvatarUrl("home.server.url", 1, 1, "", false, false)).toBeTruthy();
expect(member.getAvatarUrl("https://home.server.url", 1, 1, "", false, false)).toBeTruthy();
});
});
@@ -589,7 +589,7 @@ describe("MatrixClient syncing", () => {
name: "The Ghost",
}) as IMinimalEvent,
];
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -617,7 +617,7 @@ describe("MatrixClient syncing", () => {
name: "The Ghost",
}) as IMinimalEvent,
];
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -644,7 +644,7 @@ describe("MatrixClient syncing", () => {
});
it("should no-op if resolveInvitesToProfiles is not set", () => {
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -1373,6 +1373,114 @@ describe("MatrixClient syncing", () => {
expect(stateEventEmitCount).toEqual(2);
});
});
describe("msc4222", () => {
const roomOneSyncOne = {
"timeline": {
events: [
utils.mkMessage({
room: roomOne,
user: otherUserId,
msg: "hello",
}),
],
},
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: otherUserId,
content: {
name: "Initial room name",
},
}),
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomOne,
user: selfUserId,
content: {},
}),
],
},
};
const roomOneSyncTwo = {
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.topic",
room: roomOne,
user: selfUserId,
content: { topic: "A new room topic" },
}),
],
},
"state": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: selfUserId,
content: { name: "A new room name" },
}),
],
},
};
it("should ignore state events in timeline when state_after is present", async () => {
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncOne },
},
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncTwo },
},
});
client!.startClient();
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
const room = client!.getRoom(roomOne)!;
expect(room.name).toEqual("Initial room name");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
it("should respect state events in state_after for left rooms", async () => {
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncOne },
},
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
leave: { [roomOne]: roomOneSyncTwo },
},
});
client!.startClient();
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
const room = client!.getRoom(roomOne)!;
expect(room.name).toEqual("Initial room name");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
});
});
describe("timeline", () => {
@@ -2274,6 +2382,57 @@ describe("MatrixClient syncing", () => {
}),
]);
});
describe("msc4222", () => {
it("should respect state events in state_after for left rooms", async () => {
httpBackend!.when("POST", "/filter").respond(200, {
filter_id: "another_id",
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
leave: {
[roomOne]: {
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.topic",
room: roomOne,
user: selfUserId,
content: { topic: "A new room topic" },
}),
],
},
"state": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: selfUserId,
content: { name: "A new room name" },
}),
],
},
},
},
},
});
const [[room]] = await Promise.all([
client!.syncLeftRooms(),
// first flush the filter request; this will make syncLeftRooms make its /sync call
httpBackend!.flush("/filter").then(() => {
return httpBackend!.flushAllExpected();
}),
]);
expect(room.name).toEqual("Empty room");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
});
describe("peek", () => {
@@ -2406,7 +2565,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
it("should emit ClientEvent.Room when invited while using indexeddb crypto store", async () => {
const idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
cryptoStore: new IndexedDBCryptoStore(global.indexedDB, "tests"),
cryptoStore: new IndexedDBCryptoStore(globalThis.indexedDB, "tests"),
});
const idbHttpBackend = idbTestClient.httpBackend;
const idbClient = idbTestClient.client;
@@ -2414,7 +2573,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
await idbClient.initCrypto();
await idbClient.initLegacyCrypto();
const roomId = "!invite:example.org";
@@ -2486,7 +2645,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
store: new IndexedDBStore({
indexedDB: global.indexedDB,
indexedDB: globalThis.indexedDB,
dbName: "test",
}),
});
@@ -2558,7 +2717,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
store: new IndexedDBStore({
indexedDB: global.indexedDB,
indexedDB: globalThis.indexedDB,
dbName: "test",
}),
});
@@ -128,7 +128,7 @@ describe("MatrixClient syncing", () => {
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!;
await room.addLiveEvents([thread.rootEvent]);
await room.addLiveEvents([thread.rootEvent], { addToState: false });
// Initialize read receipt datastructure before testing the reaction
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
@@ -266,7 +266,7 @@ describe("MSC4108SignInWithQR", () => {
});
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
@@ -319,7 +319,7 @@ describe("MSC4108SignInWithQR", () => {
});
it("should not send secrets if user cancels", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
fn();
// TODO: mock timers properly
return -1 as any;
+7 -9
View File
@@ -119,7 +119,7 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto();
await client!.initLegacyCrypto();
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
}
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
@@ -601,13 +601,13 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
initial: true,
name: "Room with Invite",
required_state: [],
timeline: [
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
],
timeline: [],
});
await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name);
@@ -921,13 +921,12 @@ describe("SlidingSyncSdk", () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
required_state: [],
timeline: [
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
initial: true,
});
await emitPromise(client!, ClientEvent.Room);
@@ -962,13 +961,12 @@ describe("SlidingSyncSdk", () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
required_state: [],
timeline: [
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
initial: true,
});
const room = client!.getRoom(roomId)!;
+3 -3
View File
@@ -86,7 +86,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
};
for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
roomResponse.state!.events.push(
mkMembershipCustom({
membership: KnownMembership.Join,
sender: roomMembers[i],
@@ -560,8 +560,8 @@ export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
export type InitCrypto = (_: MatrixClient) => Promise<void>;
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
if (global.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
if (globalThis.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initLegacyCrypto();
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
+1 -1
View File
@@ -178,6 +178,6 @@ export const populateThread = ({
}: MakeThreadProps): MakeThreadResult => {
const ret = mkThread({ room, client, authorId, participantUserIds, length, ts });
ret.thread.initialEventsFetched = true;
room.addLiveEvents(ret.events);
room.addLiveEvents(ret.events, { addToState: false });
return ret;
};
+9 -6
View File
@@ -481,6 +481,9 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getUserId(): string {
return this.userId;
}
public getSafeUserId(): string {
return this.userId;
}
public getDeviceId(): string {
return this.deviceId;
@@ -582,11 +585,11 @@ export class MockCallFeed {
}
export function installWebRTCMocks() {
global.navigator = {
globalThis.navigator = {
mediaDevices: new MockMediaDevices().typed(),
} as unknown as Navigator;
global.window = {
globalThis.window = {
// @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock
@@ -596,13 +599,13 @@ export function installWebRTCMocks() {
getUserMedia: () => new MockMediaStream("local_stream"),
};
// @ts-ignore Mock
global.document = {};
globalThis.document = {};
// @ts-ignore Mock
global.AudioContext = MockAudioContext;
globalThis.AudioContext = MockAudioContext;
// @ts-ignore Mock
global.RTCRtpReceiver = {
globalThis.RTCRtpReceiver = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
codecs: [],
headerExtensions: [],
@@ -610,7 +613,7 @@ export function installWebRTCMocks() {
};
// @ts-ignore Mock
global.RTCRtpSender = {
globalThis.RTCRtpSender = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
codecs: [],
headerExtensions: [],
+2 -2
View File
@@ -22,12 +22,12 @@ import { AutoDiscovery } from "../../src/autodiscovery";
// keep to reset the fetch function after using MockHttpBackend
// @ts-ignore private property
const realAutoDiscoveryFetch: typeof global.fetch = AutoDiscovery.fetchFn;
const realAutoDiscoveryFetch: typeof globalThis.fetch = AutoDiscovery.fetchFn;
describe("AutoDiscovery", function () {
const getHttpBackend = (): MockHttpBackend => {
const httpBackend = new MockHttpBackend();
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof globalThis.fetch);
return httpBackend;
};
+1 -25
View File
@@ -15,34 +15,10 @@ limitations under the License.
*/
import { TextEncoder, TextDecoder } from "util";
import NodeBuffer from "node:buffer";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64, encodeUnpaddedBase64Url } from "../../src/base64";
describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
let origBuffer = Buffer;
beforeAll(() => {
if (env === "browser") {
origBuffer = Buffer;
// @ts-ignore
// eslint-disable-next-line no-global-assign
Buffer = undefined;
global.atob = NodeBuffer.atob;
global.btoa = NodeBuffer.btoa;
}
});
afterAll(() => {
// eslint-disable-next-line no-global-assign
Buffer = origBuffer;
// @ts-ignore
global.atob = undefined;
// @ts-ignore
global.btoa = undefined;
});
describe("Base64 encoding", () => {
it("Should decode properly encoded data", () => {
const decoded = new TextDecoder().decode(decodeBase64("ZW5jb2RpbmcgaGVsbG8gd29ybGQ="));
+2 -2
View File
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
describe("makeBeaconInfoContent()", () => {
const mockDateNow = 123456789;
beforeEach(() => {
jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
});
afterAll(() => {
jest.spyOn(global.Date, "now").mockRestore();
jest.spyOn(globalThis.Date, "now").mockRestore();
});
it("create fully defined event content", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
+25 -14
View File
@@ -63,20 +63,6 @@ describe("ContentRepo", function () {
);
});
it("should put fragments from mxc:// URIs after any query parameters", function () {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32#automade",
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
);
});
it("should return an authenticated URL when requested", function () {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, true, true)).toEqual(
@@ -98,5 +84,30 @@ describe("ContentRepo", function () {
"/_matrix/client/v1/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
);
});
it("should drop mxc urls with invalid server_name", () => {
const mxcUri = "mxc://server.name:test/foobar";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
it("should drop mxc urls with invalid media_id", () => {
const mxcUri = "mxc://server.name/foobar:test";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
it("should drop mxc urls attempting a path traversal attack", () => {
const mxcUri = "mxc://../../../../foo";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
it("should drop mxc urls attempting to pass query parameters", () => {
const mxcUri = "mxc://server.name/foobar?bar=baz";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
it("should drop mxc urls with too many parts", () => {
const mxcUri = "mxc://server.name/foo//bar";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual("");
});
});
});
+17 -17
View File
@@ -28,7 +28,7 @@ import * as testData from "../test-utils/test-data";
import { KnownMembership } from "../../src/@types/membership";
import type { DeviceInfoMap } from "../../src/crypto/DeviceList";
const Olm = global.Olm;
const Olm = globalThis.Olm;
function awaitEvent(emitter: EventEmitter, event: string): Promise<void> {
return new Promise((resolve) => {
@@ -119,7 +119,7 @@ describe("Crypto", function () {
it("getVersion() should return the current version of the olm library", async () => {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
const olmVersionTuple = Crypto.getOlmVersion();
expect(client.getCrypto()?.getVersion()).toBe(
@@ -130,7 +130,7 @@ describe("Crypto", function () {
describe("encrypted events", function () {
it("provides encryption information for events from unverified senders", async function () {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
// unencrypted event
const event = {
@@ -210,7 +210,7 @@ describe("Crypto", function () {
let client: MatrixClient;
beforeEach(async () => {
client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
// mock out the verification check
client.crypto!.checkUserTrust = (userId) => new UserTrustLevel(true, false, false);
@@ -306,7 +306,7 @@ describe("Crypto", function () {
it("doesn't throw an error when attempting to decrypt a redacted event", async () => {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
const event = new MatrixEvent({
content: {},
@@ -439,10 +439,10 @@ describe("Crypto", function () {
secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client;
bobClient = new TestClient("@bob:example.com", "bobdevice").client;
claraClient = new TestClient("@clara:example.com", "claradevice").client;
await aliceClient.initCrypto();
await secondAliceClient.initCrypto();
await bobClient.initCrypto();
await claraClient.initCrypto();
await aliceClient.initLegacyCrypto();
await secondAliceClient.initLegacyCrypto();
await bobClient.initLegacyCrypto();
await claraClient.initLegacyCrypto();
});
afterEach(async function () {
@@ -1111,7 +1111,7 @@ describe("Crypto", function () {
jest.spyOn(logger, "debug").mockImplementation(() => {});
jest.setTimeout(10000);
const client = new TestClient("@a:example.com", "dev").client;
await client.initCrypto();
await client.initLegacyCrypto();
client.crypto!.isCrossSigningReady = async () => false;
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
@@ -1147,9 +1147,9 @@ describe("Crypto", function () {
client = new TestClient("@alice:example.org", "aliceweb");
// running initCrypto should trigger a key upload
// running initLegacyCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
@@ -1264,9 +1264,9 @@ describe("Crypto", function () {
client = new TestClient("@alice:example.org", "aliceweb");
// running initCrypto should trigger a key upload
// running initLegacyCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
@@ -1362,7 +1362,7 @@ describe("Crypto", function () {
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
await client.client.initLegacyCrypto();
});
afterEach(async () => {
@@ -1388,7 +1388,7 @@ describe("Crypto", function () {
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
await client.client.initLegacyCrypto();
});
afterEach(async () => {
@@ -1414,7 +1414,7 @@ describe("Crypto", function () {
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
await client.client.initLegacyCrypto();
});
afterEach(async function () {
+4 -4
View File
@@ -44,13 +44,13 @@ badKey[0] ^= 1;
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("should throw if no callback is provided", async () => {
@@ -80,7 +80,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
expect(pubKey).toEqual(masterKeyPub);
// check that the pkSigning object corresponds to the pubKey
const signature = pkSigning.sign("message");
const util = new global.Olm.Utility();
const util = new globalThis.Olm.Utility();
try {
util.ed25519_verify(pubKey, "message", signature);
} finally {
@@ -199,7 +199,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
* it's not possible to get one in normal execution unless you hack as we do here.
*/
describe.each([
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new IndexedDBCryptoStore(undefined!, "tests")],
[
"MemoryCryptoStore",
+13 -9
View File
@@ -43,10 +43,10 @@ const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2
const ROOM_ID = "!ROOM:ID";
const Olm = global.Olm;
const Olm = globalThis.Olm;
describe("MegolmDecryption", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm unit tests: libolm not present");
return;
}
@@ -101,7 +101,7 @@ describe("MegolmDecryption", function () {
describe("receives some keys:", function () {
let groupSession: OutboundGroupSession;
beforeEach(async function () {
groupSession = new global.Olm.OutboundGroupSession();
groupSession = new globalThis.Olm.OutboundGroupSession();
groupSession.create();
// construct a fake decrypted key event via the use of a mocked
@@ -610,7 +610,11 @@ describe("MegolmDecryption", function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client;
const bobClient2 = new TestClient("@bob:example.com", "bobdevice2").client;
await Promise.all([aliceClient.initCrypto(), bobClient1.initCrypto(), bobClient2.initCrypto()]);
await Promise.all([
aliceClient.initLegacyCrypto(),
bobClient1.initLegacyCrypto(),
bobClient2.initLegacyCrypto(),
]);
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice1 = bobClient1.crypto!.olmDevice;
const bobDevice2 = bobClient2.crypto!.olmDevice;
@@ -704,7 +708,7 @@ describe("MegolmDecryption", function () {
it("does not block unverified devices when sending verification events", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const bobDevice = bobClient.crypto!.olmDevice;
const encryptionCfg = {
@@ -789,7 +793,7 @@ describe("MegolmDecryption", function () {
it("notifies devices when unable to create olm session", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice = bobClient.crypto!.olmDevice;
@@ -873,7 +877,7 @@ describe("MegolmDecryption", function () {
it("throws an error describing why it doesn't have a key", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const bobDevice = bobClient.crypto!.olmDevice;
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
@@ -955,7 +959,7 @@ describe("MegolmDecryption", function () {
it("throws an error describing the lack of an olm session", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
@@ -1051,7 +1055,7 @@ describe("MegolmDecryption", function () {
it("throws an error to indicate a wedged olm session", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
+2 -2
View File
@@ -47,13 +47,13 @@ function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
}
describe("OlmDevice", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
let aliceOlmDevice: OlmDevice;
+20 -12
View File
@@ -33,7 +33,7 @@ import { CryptoStore } from "../../../src/crypto/store/base";
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
const Olm = global.Olm;
const Olm = globalThis.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
@@ -155,7 +155,7 @@ function makeTestClient(cryptoStore: CryptoStore) {
}
describe("MegolmBackup", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
@@ -205,14 +205,14 @@ describe("MegolmBackup", function () {
// clobber the setTimeout function to run 100x faster.
// ideally we would use lolex, but we have no oportunity
// to tick the clock between the first try and the retry.
const realSetTimeout = global.setTimeout;
jest.spyOn(global, "setTimeout").mockImplementation(function (f, n) {
const realSetTimeout = globalThis.setTimeout;
jest.spyOn(globalThis, "setTimeout").mockImplementation(function (f, n) {
return realSetTimeout(f!, n! / 100);
});
});
afterEach(function () {
jest.spyOn(global, "setTimeout").mockRestore();
jest.spyOn(globalThis, "setTimeout").mockRestore();
});
test("fail if crypto not enabled", async () => {
@@ -231,7 +231,7 @@ describe("MegolmBackup", function () {
test("fail if given backup has no version", async () => {
const client = makeTestClient(cryptoStore);
await client.initCrypto();
await client.initLegacyCrypto();
const data = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: {
@@ -314,7 +314,7 @@ describe("MegolmBackup", function () {
megolmDecryption.olmlib = mockOlmLib;
return client
.initCrypto()
.initLegacyCrypto()
.then(() => {
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
@@ -391,7 +391,7 @@ describe("MegolmBackup", function () {
megolmDecryption.olmlib = mockOlmLib;
return client
.initCrypto()
.initLegacyCrypto()
.then(() => {
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
})
@@ -471,7 +471,7 @@ describe("MegolmBackup", function () {
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
await client.initLegacyCrypto();
client.uploadDeviceSigningKeys = async function (e) {
return {};
};
@@ -560,7 +560,7 @@ describe("MegolmBackup", function () {
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
await client.initLegacyCrypto();
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -636,7 +636,7 @@ describe("MegolmBackup", function () {
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
return client.initLegacyCrypto();
});
afterEach(function () {
@@ -773,11 +773,19 @@ describe("MegolmBackup", function () {
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
await client.initCrypto();
await client.initLegacyCrypto();
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6);
client.stopClient();
});
});
describe("getKeyBackupInfo", () => {
it("should return throw an `Not implemented`", async () => {
const client = makeTestClient(cryptoStore);
await client.initLegacyCrypto();
await expect(client.getCrypto()?.getKeyBackupInfo()).rejects.toThrow("Not implemented");
});
});
});
+20 -20
View File
@@ -78,19 +78,19 @@ async function makeTestClient(
const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
const client = testClient.client;
await client.initCrypto();
await client.initLegacyCrypto();
return { client, httpBackend: testClient.httpBackend };
}
describe("Cross Signing", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("should sign the master key with the device key", async function () {
@@ -369,10 +369,10 @@ describe("Cross Signing", function () {
// set Alice's cross-signing key
await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterSigning = new globalThis.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
const bobSigning = new global.Olm.PkSigning();
const bobSigning = new globalThis.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: CrossSigningKeyInfo = {
@@ -488,7 +488,7 @@ describe("Cross Signing", function () {
};
await alice.crypto!.signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account();
const bobOlmAccount = new globalThis.Olm.Account();
bobOlmAccount.create();
const bobKeys = JSON.parse(bobOlmAccount.identity_keys());
const bobDeviceUnsigned = {
@@ -622,10 +622,10 @@ describe("Cross Signing", function () {
await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
// (NOTE: device key is not signed by ssk)
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterSigning = new globalThis.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
const bobSigning = new global.Olm.PkSigning();
const bobSigning = new globalThis.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: CrossSigningKeyInfo = {
@@ -688,10 +688,10 @@ describe("Cross Signing", function () {
alice.uploadKeySignatures = async () => ({ failures: {} });
await resetCrossSigningKeys(alice);
// Alice downloads Bob's keys
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterSigning = new globalThis.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
const bobSigning = new global.Olm.PkSigning();
const bobSigning = new globalThis.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: CrossSigningKeyInfo = {
@@ -755,10 +755,10 @@ describe("Cross Signing", function () {
expect(bobDeviceTrust.isTofu()).toBeTruthy();
// Alice downloads new SSK for Bob
const bobMasterSigning2 = new global.Olm.PkSigning();
const bobMasterSigning2 = new globalThis.Olm.PkSigning();
const bobMasterPrivkey2 = bobMasterSigning2.generate_seed();
const bobMasterPubkey2 = bobMasterSigning2.init_with_seed(bobMasterPrivkey2);
const bobSigning2 = new global.Olm.PkSigning();
const bobSigning2 = new globalThis.Olm.PkSigning();
const bobPrivkey2 = bobSigning2.generate_seed();
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
const bobSSK2: CrossSigningKeyInfo = {
@@ -905,10 +905,10 @@ describe("Cross Signing", function () {
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterSigning = new globalThis.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const aliceSigning = new globalThis.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: CrossSigningKeyInfo = {
@@ -980,10 +980,10 @@ describe("Cross Signing", function () {
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterSigning = new globalThis.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const aliceSigning = new globalThis.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: CrossSigningKeyInfo = {
@@ -1040,10 +1040,10 @@ describe("Cross Signing", function () {
alice.uploadKeySignatures = async () => ({ failures: {} });
// Generate Alice's SSK etc
const aliceMasterSigning = new global.Olm.PkSigning();
const aliceMasterSigning = new globalThis.Olm.PkSigning();
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
const aliceSigning = new global.Olm.PkSigning();
const aliceSigning = new globalThis.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: CrossSigningKeyInfo = {
@@ -1088,12 +1088,12 @@ describe("Cross Signing", function () {
});
describe("userHasCrossSigningKeys", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
return;
}
beforeAll(() => {
return global.Olm.init();
return globalThis.Olm.init();
});
let aliceClient: MatrixClient;
+1 -1
View File
@@ -32,7 +32,7 @@ export async function resetCrossSigningKeys(
}
export async function createSecretStorageKey(): Promise<IRecoveryKey> {
const decryption = new global.Olm.PkDecryption();
const decryption = new globalThis.Olm.PkDecryption();
decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
decryption.free();
+4 -4
View File
@@ -19,16 +19,16 @@ import { TestClient } from "../../TestClient";
import { logger } from "../../../src/logger";
import { DEHYDRATION_ALGORITHM } from "../../../src/crypto/dehydration";
const Olm = global.Olm;
const Olm = globalThis.Olm;
describe("Dehydration", () => {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running dehydration unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("should rehydrate a dehydrated device", async () => {
@@ -68,7 +68,7 @@ describe("Dehydration", () => {
},
});
await alice.client.initCrypto();
await alice.client.initLegacyCrypto();
alice.httpBackend.when("GET", "/room_keys/version").respond(404, {
errcode: "M_NOT_FOUND",
@@ -51,7 +51,7 @@ const requests = [
];
describe.each([
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("Outgoing room key requests [%s]", function (name, dbFactory) {
+5 -5
View File
@@ -43,7 +43,7 @@ async function makeTestClient(
return true;
};
await client.initCrypto();
await client.initLegacyCrypto();
// No need to download keys for these tests
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map());
@@ -69,20 +69,20 @@ function sign<T extends IObject | ICurve25519AuthData>(
}
describe("Secrets", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("should store and retrieve a secret", async function () {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const signing = new global.Olm.PkSigning();
const signing = new globalThis.Olm.PkSigning();
const signingKey = signing.generate_seed();
const signingPubKey = signing.init_with_seed(signingKey);
@@ -332,7 +332,7 @@ describe("Secrets", function () {
});
it("bootstraps when cross-signing keys in secret storage", async function () {
const decryption = new global.Olm.PkDecryption();
const decryption = new globalThis.Olm.PkDecryption();
const storagePrivateKey = decryption.get_private_key();
const bob: MatrixClient = await makeTestClient(
+1 -1
View File
@@ -20,7 +20,7 @@ import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from
import { CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
describe.each([
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("CryptoStore tests for %s", function (name, dbFactory) {
@@ -27,21 +27,21 @@ describe("IndexedDBCryptoStore", () => {
it("Should be true if there is a legacy database", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
await store.startup();
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
expect(result).toBe(true);
});
it("Should be true if there is a legacy database in non migrated state", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
await store.startup();
await store.setMigrationState(MigrationState.NOT_STARTED);
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
expect(result).toBe(true);
});
@@ -54,18 +54,18 @@ describe("IndexedDBCryptoStore", () => {
])("Exists and Migration state is %s", (migrationState) => {
it("Should be false if migration has started", async () => {
// should detect a store that is not migrated
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
await store.startup();
await store.setMigrationState(migrationState);
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
expect(result).toBe(false);
});
});
it("Should be false if there is no legacy database", async () => {
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
expect(result).toBe(false);
});
@@ -17,10 +17,10 @@ limitations under the License.
import "../../../olm-loader";
import { logger } from "../../../../src/logger";
const Olm = global.Olm;
const Olm = globalThis.Olm;
describe("QR code verification", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running device verification tests: libolm not present");
return;
}
@@ -20,12 +20,12 @@ import { logger } from "../../../../src/logger";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { makeTestClients } from "./util";
const Olm = global.Olm;
const Olm = globalThis.Olm;
jest.useFakeTimers();
describe("verification request integration tests with crypto layer", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running device verification unit tests: libolm not present");
return;
}
+2 -2
View File
@@ -29,13 +29,13 @@ import { MatrixClient } from "../../../../src";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
import { TestClient } from "../../../TestClient";
const Olm = global.Olm;
const Olm = globalThis.Olm;
let ALICE_DEVICES: Record<string, IDevice>;
let BOB_DEVICES: Record<string, IDevice>;
describe("SAS verification", function () {
if (!global.Olm) {
if (!globalThis.Olm) {
logger.warn("Not running device verification unit tests: libolm not present");
return;
}
@@ -34,7 +34,7 @@ const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("self-verifications", () => {
beforeAll(function () {
return global.Olm.init();
return globalThis.Olm.init();
});
it("triggers a request for key sharing upon completion", async () => {
+1 -1
View File
@@ -119,7 +119,7 @@ export async function makeTestClients(
clients.push(testClient);
}
await Promise.all(clients.map((testClient) => testClient.client.initCrypto()));
await Promise.all(clients.map((testClient) => testClient.client.initLegacyCrypto()));
const destroy = () => {
timeouts.forEach((t) => clearTimeout(t));
+3 -3
View File
@@ -29,12 +29,12 @@ describe("sha256", () => {
});
it("throws if webcrypto is not available", async () => {
const oldCrypto = global.crypto;
const oldCrypto = globalThis.crypto;
try {
global.crypto = {} as any;
globalThis.crypto = {} as any;
await expect(sha256("test")).rejects.toThrow();
} finally {
global.crypto = oldCrypto;
globalThis.crypto = oldCrypto;
}
});
});
+258 -4
View File
@@ -30,15 +30,18 @@ import {
ITurnServer,
IRoomEvent,
IOpenIDCredentials,
ISendEventFromWidgetResponseData,
WidgetApiResponseError,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded";
import { ICapabilities, RoomWidgetClient } from "../../src/embedded";
import { MatrixEvent } from "../../src/models/event";
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { sleep } from "../../src/utils";
const testOIDCToken = {
access_token: "12345678",
@@ -90,7 +93,11 @@ class MockWidgetApi extends EventEmitter {
public getTurnServers = jest.fn(() => []);
public sendContentLoaded = jest.fn();
public transport = { reply: jest.fn() };
public transport = {
reply: jest.fn(),
send: jest.fn(),
sendComplete: jest.fn(),
};
}
declare module "../../src/types" {
@@ -122,9 +129,16 @@ describe("RoomWidgetClient", () => {
const makeClient = async (
capabilities: ICapabilities,
sendContentLoaded: boolean | undefined = undefined,
userId?: string,
): Promise<void> => {
const baseUrl = "https://example.org";
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl }, sendContentLoaded);
client = createRoomWidgetClient(
widgetApi,
capabilities,
"!1:example.org",
{ baseUrl, userId },
sendContentLoaded,
);
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
widgetApi.emit("ready");
await client.startClient();
@@ -187,6 +201,182 @@ describe("RoomWidgetClient", () => {
.map((e) => e.getEffectiveEvent()),
).toEqual([event]);
});
describe("local echos", () => {
const setupRemoteEcho = () => {
makeClient(
{
receiveEvent: ["org.matrix.rageshake_request"],
sendEvent: ["org.matrix.rageshake_request"],
},
undefined,
"@me:example.org",
);
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
const injectSpy = jest.spyOn((client as any).syncApi, "injectRoomEvents");
const widgetSendEmitter = new EventEmitter();
const widgetSendPromise = new Promise<void>((resolve) =>
widgetSendEmitter.once("send", () => resolve()),
);
const resolveWidgetSend = () => widgetSendEmitter.emit("send");
widgetApi.sendRoomEvent.mockImplementation(
async (eType, content, roomId): Promise<ISendEventFromWidgetResponseData> => {
await widgetSendPromise;
return { room_id: "!1:example.org", event_id: "event_id" };
},
);
return { injectSpy, resolveWidgetSend };
};
const remoteEchoEvent = new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
detail: {
data: {
type: "org.matrix.rageshake_request",
room_id: "!1:example.org",
event_id: "event_id",
sender: "@me:example.org",
state_key: "bar",
content: { hello: "world" },
unsigned: { transaction_id: "1234" },
},
},
cancelable: true,
});
it("get response then local echo", async () => {
await sleep(600);
const { injectSpy, resolveWidgetSend } = await setupRemoteEcho();
// Begin by sending an event:
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 12 }, "widgetTxId");
// we do not expect it to be send -- until we call `resolveWidgetSend`
expect(injectSpy).not.toHaveBeenCalled();
// We first get the response from the widget
resolveWidgetSend();
// We then get the remote echo from the widget
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, remoteEchoEvent);
// gets emitted after the event got injected
await new Promise<void>((resolve) => client.once(ClientEvent.Event, () => resolve()));
expect(injectSpy).toHaveBeenCalled();
const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
});
it("get local echo then response", async () => {
await sleep(600);
const { injectSpy, resolveWidgetSend } = await setupRemoteEcho();
// Begin by sending an event:
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 12 }, "widgetTxId");
// we do not expect it to be send -- until we call `resolveWidgetSend`
expect(injectSpy).not.toHaveBeenCalled();
// We first get the remote echo from the widget
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, remoteEchoEvent);
expect(injectSpy).not.toHaveBeenCalled();
// We then get the response from the widget
resolveWidgetSend();
// Gets emitted after the event got injected
await new Promise<void>((resolve) => client.once(ClientEvent.Event, () => resolve()));
expect(injectSpy).toHaveBeenCalled();
const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
});
it("__ local echo then response", async () => {
await sleep(600);
const { injectSpy, resolveWidgetSend } = await setupRemoteEcho();
// Begin by sending an event:
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 12 }, "widgetTxId");
// we do not expect it to be send -- until we call `resolveWidgetSend`
expect(injectSpy).not.toHaveBeenCalled();
// We first get the remote echo from the widget
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, remoteEchoEvent);
const otherRemoteEcho = new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
detail: { data: { ...remoteEchoEvent.detail.data } },
});
otherRemoteEcho.detail.data.unsigned.transaction_id = "4567";
otherRemoteEcho.detail.data.event_id = "other_id";
widgetApi.emit(`action:${WidgetApiToWidgetAction.SendEvent}`, otherRemoteEcho);
// Simulate the wait time while the widget is waiting for a response
// after we already received the remote echo
await sleep(20);
// even after the wait we do not want any event to be injected.
// we do not know their event_id and cannot know if they are the remote echo
// where we need to update the txId because they are send by this client
expect(injectSpy).not.toHaveBeenCalled();
// We then get the response from the widget
resolveWidgetSend();
// Gets emitted after the event got injected
await new Promise<void>((resolve) => client.once(ClientEvent.Event, () => resolve()));
// Now we want both events to be injected since we know the txId - event_id match
expect(injectSpy).toHaveBeenCalled();
// it has been called with the event sent by ourselves
const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
// It has been called by the event we blocked because of our send right afterwards
const call2 = injectSpy.mock.calls[1] as any;
const injectedEv2 = call2[3][0];
expect(injectedEv2.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv2.getUnsigned().transaction_id).toBe("4567");
});
});
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to send");
widgetApi.transport.send.mockRejectedValue(error);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow(error);
});
it("handles widget errors with Matrix API error response data", async () => {
const errorStatusCode = 400;
const errorUrl = "http://example.org";
const errorData = {
errcode: "M_BAD_JSON",
error: "Invalid body",
};
const widgetError = new WidgetApiResponseError("failed to send", {
matrix_api_error: {
http_status: errorStatusCode,
http_headers: {},
url: errorUrl,
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
widgetApi.transport.send.mockRejectedValue(widgetError);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow(matrixError);
});
});
describe("delayed events", () => {
@@ -493,6 +683,23 @@ describe("RoomWidgetClient", () => {
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
};
const encryptedContentMap = new Map<string, Map<string, object>>([
["@alice:example.org", new Map([["aliceMobile", { hello: "alice!" }]])],
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
]);
it("sends unencrypted (sendToDeviceViaWidgetApi)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
await (client as RoomWidgetClient).sendToDeviceViaWidgetApi(
"org.example.foo",
false,
unencryptedContentMap,
);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
});
it("sends unencrypted (sendToDevice)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
@@ -534,6 +741,17 @@ describe("RoomWidgetClient", () => {
});
});
it("sends encrypted (sendToDeviceViaWidgetApi)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
await (client as RoomWidgetClient).sendToDeviceViaWidgetApi("org.example.foo", true, encryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
"@alice:example.org": { aliceMobile: { hello: "alice!" } },
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
});
});
it.each([
{ encrypted: false, title: "unencrypted" },
{ encrypted: true, title: "encrypted" },
@@ -570,6 +788,42 @@ describe("RoomWidgetClient", () => {
await makeClient({});
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
});
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to get token");
widgetApi.transport.sendComplete.mockRejectedValue(error);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
await expect(client.getOpenIdToken()).rejects.toThrow(error);
});
it("handles widget errors with Matrix API error response data", async () => {
const errorStatusCode = 400;
const errorUrl = "http://example.org";
const errorData = {
errcode: "M_UNKNOWN",
error: "Bad request",
};
const widgetError = new WidgetApiResponseError("failed to get token", {
matrix_api_error: {
http_status: errorStatusCode,
http_headers: {},
url: errorUrl,
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
await expect(client.getOpenIdToken()).rejects.toThrow(matrixError);
});
});
it("gets TURN servers", async () => {
+2 -2
View File
@@ -74,7 +74,7 @@ describe("eventMapperFor", function () {
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
@@ -109,7 +109,7 @@ describe("eventMapperFor", function () {
room.oldState.setStateEvents([event]);
room.currentState.setStateEvents([event]);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
+21 -5
View File
@@ -104,7 +104,7 @@ describe("EventTimelineSet", () => {
it("Adds event to the live timeline in the timeline set", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent);
eventTimelineSet.addLiveEvent(messageEvent, { addToState: false });
expect(liveTimeline.getEvents().length).toStrictEqual(1);
});
@@ -113,6 +113,7 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent, {
duplicateStrategy: DuplicateStrategy.Replace,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(1);
@@ -130,6 +131,7 @@ describe("EventTimelineSet", () => {
// replace.
eventTimelineSet.addLiveEvent(duplicateMessageEvent, {
duplicateStrategy: DuplicateStrategy.Replace,
addToState: false,
});
const eventsInLiveTimeline = liveTimeline.getEvents();
@@ -144,6 +146,7 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(1);
});
@@ -151,10 +154,17 @@ describe("EventTimelineSet", () => {
it("Make sure legacy overload passing options directly as parameters still works", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
}).not.toThrow();
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
fromCache: false,
addToState: false,
});
}).not.toThrow();
});
@@ -167,11 +177,13 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(reactionEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents()).toHaveLength(1);
const [event] = liveTimeline.getEvents();
@@ -202,6 +214,7 @@ describe("EventTimelineSet", () => {
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
toStartOfTimeline: true,
addToState: false,
});
}).toThrow();
});
@@ -214,6 +227,7 @@ describe("EventTimelineSet", () => {
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
@@ -232,6 +246,7 @@ describe("EventTimelineSet", () => {
eventTimelineSetForThread.addEventToTimeline(normalMessage, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
@@ -248,6 +263,7 @@ describe("EventTimelineSet", () => {
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
});
@@ -257,7 +273,7 @@ describe("EventTimelineSet", () => {
describe("aggregateRelations", () => {
describe("with unencrypted events", () => {
beforeEach(() => {
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
});
itShouldReturnTheRelatedEvents();
@@ -279,7 +295,7 @@ describe("EventTimelineSet", () => {
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
});
it("should not return the related events", () => {
+27 -23
View File
@@ -98,7 +98,7 @@ describe("EventTimeline", function () {
expect(function () {
timeline.initialiseState(state);
}).not.toThrow();
timeline.addEvent(event, { toStartOfTimeline: false });
timeline.addEvent(event, { toStartOfTimeline: false, addToState: false });
expect(function () {
timeline.initialiseState(state);
}).toThrow();
@@ -182,9 +182,9 @@ describe("EventTimeline", function () {
];
it("should be able to add events to the end", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[0]);
@@ -192,9 +192,9 @@ describe("EventTimeline", function () {
});
it("should be able to add events to the start", function () {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[1]);
@@ -238,9 +238,9 @@ describe("EventTimeline", function () {
content: { name: "Old Room Name" },
});
timeline.addEvent(newEv, { toStartOfTimeline: false });
timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
expect(newEv.sender).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
expect(oldEv.sender).toEqual(oldSentinel);
});
@@ -280,9 +280,9 @@ describe("EventTimeline", function () {
skey: userA,
event: true,
});
timeline.addEvent(newEv, { toStartOfTimeline: false });
timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
expect(oldEv.target).toEqual(oldSentinel);
});
@@ -308,8 +308,8 @@ describe("EventTimeline", function () {
}),
];
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: true });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: true });
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined,
@@ -347,8 +347,8 @@ describe("EventTimeline", function () {
}),
];
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: true });
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: true });
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined,
@@ -365,11 +365,15 @@ describe("EventTimeline", function () {
);
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false })).not.toThrow();
// @ts-ignore stateContext is not a valid param
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
expect(() =>
timeline.addEvent(events[0], { toStartOfTimeline: false, roomState: new RoomState(roomId) }),
timeline.addEvent(events[0], {
toStartOfTimeline: false,
addToState: false,
roomState: new RoomState(roomId),
}),
).not.toThrow();
});
});
@@ -397,8 +401,8 @@ describe("EventTimeline", function () {
];
it("should remove events", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
expect(timeline.getEvents().length).toEqual(2);
let ev = timeline.removeEvent(events[0].getId()!);
@@ -411,9 +415,9 @@ describe("EventTimeline", function () {
});
it("should update baseIndex", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[2], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1);
@@ -430,11 +434,11 @@ describe("EventTimeline", function () {
// - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event", function () {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
timeline.removeEvent(events[0].getId()!);
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[2], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
+189
View File
@@ -0,0 +1,189 @@
/*
Copyright 2024 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 "../../../src";
type IErrorJson = MatrixError["data"];
describe("MatrixError", () => {
let headers: Headers;
beforeEach(() => {
headers = new Headers({ "Content-Type": "application/json" });
});
function makeMatrixError(httpStatus: number, data: IErrorJson, url?: string): MatrixError {
return new MatrixError(data, httpStatus, url, undefined, headers);
}
it("should accept absent retry time from rate-limit error", () => {
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
expect(err.isRateLimitError()).toBe(true);
expect(err.getRetryAfterMs()).toEqual(null);
});
it("should retrieve retry_after_ms from rate-limit error", () => {
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
expect(err.getRetryAfterMs()).toEqual(150000);
});
it("should ignore retry_after_ms if errcode is not M_LIMIT_EXCEEDED", () => {
const err = makeMatrixError(429, { errcode: "M_UNKNOWN", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
expect(err.getRetryAfterMs()).toEqual(null);
});
it("should retrieve numeric Retry-After header from rate-limit error", () => {
headers.set("Retry-After", "120");
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
// prefer Retry-After header over retry_after_ms
expect(err.getRetryAfterMs()).toEqual(120000);
});
it("should retrieve Date Retry-After header from rate-limit error", () => {
headers.set("Retry-After", `${new Date(160000).toUTCString()}`);
jest.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
expect(err.isRateLimitError()).toBe(true);
// prefer Retry-After header over retry_after_ms
expect(err.getRetryAfterMs()).toEqual(60000);
});
it("should prefer M_FORBIDDEN errcode over HTTP status code 429", () => {
headers.set("Retry-After", "120");
const err = makeMatrixError(429, { errcode: "M_FORBIDDEN" });
expect(err.isRateLimitError()).toBe(false);
// retrieve Retry-After header even for non-M_LIMIT_EXCEEDED errors
expect(err.getRetryAfterMs()).toEqual(120000);
});
it("should prefer M_LIMIT_EXCEEDED errcode over HTTP status code 400", () => {
headers.set("Retry-After", "120");
const err = makeMatrixError(400, { errcode: "M_LIMIT_EXCEEDED" });
expect(err.isRateLimitError()).toBe(true);
// retrieve Retry-After header even for non-429 errors
expect(err.getRetryAfterMs()).toEqual(120000);
});
it("should reject invalid Retry-After header", () => {
for (const invalidValue of ["-1", "1.23", new Date(0).toString()]) {
headers.set("Retry-After", invalidValue);
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
expect(() => err.getRetryAfterMs()).toThrow(
"value is not a valid HTTP-date or non-negative decimal integer",
);
}
});
it("should reject too-large Retry-After header", () => {
headers.set("Retry-After", "1" + Array(500).fill("0").join(""));
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
expect(() => err.getRetryAfterMs()).toThrow("integer value is too large");
});
describe("can be converted to data compatible with the widget api", () => {
it("from default values", () => {
const matrixError = new MatrixError();
const widgetApiErrorData = {
http_status: 400,
http_headers: {},
url: "",
response: {
errcode: "M_UNKNOWN",
error: "Unknown message",
},
};
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
});
it("from non-default values", () => {
headers.set("Retry-After", "120");
const statusCode = 429;
const data = {
errcode: "M_LIMIT_EXCEEDED",
error: "Request is rate-limited.",
retry_after_ms: 120000,
};
const url = "http://example.net";
const matrixError = makeMatrixError(statusCode, data, url);
const widgetApiErrorData = {
http_status: statusCode,
http_headers: {
"content-type": "application/json",
"retry-after": "120",
},
url,
response: data,
};
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
});
});
describe("can be created from data received from the widget api", () => {
it("from minimal data", () => {
const statusCode = 400;
const data = {
errcode: "M_UNKNOWN",
error: "Something went wrong.",
};
const url = "";
const widgetApiErrorData = {
http_status: statusCode,
http_headers: {},
url,
response: data,
};
headers.delete("Content-Type");
const matrixError = makeMatrixError(statusCode, data, url);
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
});
it("from more data", () => {
const statusCode = 429;
const data = {
errcode: "M_LIMIT_EXCEEDED",
error: "Request is rate-limited.",
retry_after_ms: 120000,
};
const url = "http://example.net";
const widgetApiErrorData = {
http_status: statusCode,
http_headers: {
"content-type": "application/json",
"retry-after": "120",
},
url,
response: data,
};
headers.set("Retry-After", "120");
const matrixError = makeMatrixError(statusCode, data, url);
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
});
});
});
+3 -3
View File
@@ -60,11 +60,11 @@ describe("FetchHttpApi", () => {
});
it("should fall back to global fetch if fetchFn not provided", () => {
global.fetch = jest.fn();
expect(global.fetch).not.toHaveBeenCalled();
globalThis.fetch = jest.fn();
expect(globalThis.fetch).not.toHaveBeenCalled();
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
api.fetch("test");
expect(global.fetch).toHaveBeenCalled();
expect(globalThis.fetch).toHaveBeenCalled();
});
it("should update identity server base url", () => {
+9 -5
View File
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022 - 2024 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.
@@ -41,12 +41,13 @@ describe("MatrixHttpApi", () => {
setRequestHeader: jest.fn(),
onreadystatechange: undefined,
getResponseHeader: jest.fn(),
getAllResponseHeaders: jest.fn(),
} as unknown as XMLHttpRequest;
// We stub out XHR here as it is not available in JSDOM
// @ts-ignore
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
globalThis.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
// @ts-ignore
global.XMLHttpRequest.DONE = DONE;
globalThis.XMLHttpRequest.DONE = DONE;
});
afterEach(() => {
@@ -59,7 +60,7 @@ describe("MatrixHttpApi", () => {
});
it("should fall back to `fetch` where xhr is unavailable", () => {
global.XMLHttpRequest = undefined!;
globalThis.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);
@@ -171,7 +172,10 @@ describe("MatrixHttpApi", () => {
xhr.readyState = DONE;
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
xhr.status = 404;
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
mocked(xhr.getResponseHeader).mockImplementation((name) =>
name.toLowerCase() === "content-type" ? "application/json" : null,
);
mocked(xhr.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
// @ts-ignore
xhr.onreadystatechange?.(new Event("test"));
+107 -35
View File
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022 - 2024 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.
@@ -86,13 +86,28 @@ describe("anySignal", () => {
});
describe("parseErrorResponse", () => {
let headers: Headers;
const xhrHeaderMethods = {
getResponseHeader: (name: string) => headers.get(name),
getAllResponseHeaders: () => {
let allHeaders = "";
headers.forEach((value, key) => {
allHeaders += `${key.toLowerCase()}: ${value}\r\n`;
});
return allHeaders;
},
};
beforeEach(() => {
headers = new Headers();
});
it("should resolve Matrix Errors from XHR", () => {
headers.set("Content-Type", "application/json");
expect(
parseErrorResponse(
{
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
...xhrHeaderMethods,
status: 500,
} as XMLHttpRequest,
'{"errcode": "TEST"}',
@@ -108,14 +123,11 @@ describe("parseErrorResponse", () => {
});
it("should resolve Matrix Errors from fetch", () => {
headers.set("Content-Type", "application/json");
expect(
parseErrorResponse(
{
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
@@ -131,13 +143,12 @@ describe("parseErrorResponse", () => {
});
it("should resolve Matrix Errors from XHR with urls", () => {
headers.set("Content-Type", "application/json");
expect(
parseErrorResponse(
{
responseURL: "https://example.com",
getResponseHeader(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
...xhrHeaderMethods,
status: 500,
} as XMLHttpRequest,
'{"errcode": "TEST"}',
@@ -154,15 +165,12 @@ describe("parseErrorResponse", () => {
});
it("should resolve Matrix Errors from fetch with urls", () => {
headers.set("Content-Type", "application/json");
expect(
parseErrorResponse(
{
url: "https://example.com",
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
@@ -178,6 +186,66 @@ describe("parseErrorResponse", () => {
);
});
describe("with HTTP headers", () => {
function addHeaders(headers: Headers) {
headers.set("Age", "0");
headers.set("Date", "Thu, 01 Jan 1970 00:00:00 GMT"); // value contains colons
headers.set("x-empty", "");
headers.set("x-multi", "1");
headers.append("x-multi", "2");
}
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) {
expect(new Map(otherHeaders)).toEqual(new Map(expectedHeaders));
}
it("should resolve HTTP Errors from XHR with headers", () => {
headers.set("Content-Type", "text/plain");
addHeaders(headers);
const err = parseErrorResponse({
...xhrHeaderMethods,
status: 500,
} as XMLHttpRequest) as HTTPError;
compareHeaders(headers, err.httpHeaders);
});
it("should resolve HTTP Errors from fetch with headers", () => {
headers.set("Content-Type", "text/plain");
addHeaders(headers);
const err = parseErrorResponse({
headers,
status: 500,
} as Response) as HTTPError;
compareHeaders(headers, err.httpHeaders);
});
it("should resolve Matrix Errors from XHR with headers", () => {
headers.set("Content-Type", "application/json");
addHeaders(headers);
const err = parseErrorResponse(
{
...xhrHeaderMethods,
status: 500,
} as XMLHttpRequest,
'{"errcode": "TEST"}',
) as MatrixError;
compareHeaders(headers, err.httpHeaders);
});
it("should resolve Matrix Errors from fetch with headers", () => {
headers.set("Content-Type", "application/json");
addHeaders(headers);
const err = parseErrorResponse(
{
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
) as MatrixError;
compareHeaders(headers, err.httpHeaders);
});
});
it("should set a sensible default error message on MatrixError", () => {
let err = new MatrixError();
expect(err.message).toEqual("MatrixError: Unknown message");
@@ -188,14 +256,11 @@ describe("parseErrorResponse", () => {
});
it("should handle no type gracefully", () => {
// No Content-Type header
expect(
parseErrorResponse(
{
headers: {
get(name: string): string | null {
return null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
@@ -203,31 +268,38 @@ describe("parseErrorResponse", () => {
).toStrictEqual(new HTTPError("Server returned 500 error", 500));
});
it("should handle invalid type gracefully", () => {
it("should handle empty type gracefully", () => {
headers.set("Content-Type", " ");
expect(
parseErrorResponse(
{
headers: {
get(name: string): string | null {
return name === "Content-Type" ? " " : null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
).toStrictEqual(new Error("Error parsing Content-Type '': TypeError: argument string is required"));
});
it("should handle plaintext errors", () => {
it("should handle invalid type gracefully", () => {
headers.set("Content-Type", "unknown");
expect(
parseErrorResponse(
{
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "text/plain" : null;
},
},
headers,
status: 500,
} as Response,
'{"errcode": "TEST"}',
),
).toStrictEqual(new Error("Error parsing Content-Type 'unknown': TypeError: invalid media type"));
});
it("should handle plaintext errors", () => {
headers.set("Content-Type", "text/plain");
expect(
parseErrorResponse(
{
headers,
status: 418,
} as Response,
"I'm a teapot",
+28 -24
View File
@@ -2799,24 +2799,28 @@ describe("MatrixClient", function () {
roomCreateEvent(room1.roomId, replacedByCreate1.roomId),
predecessorEvent(room1.roomId, replacedByDynamicPredecessor1.roomId),
],
{},
{ addToState: true },
);
room2.addLiveEvents(
[
roomCreateEvent(room2.roomId, replacedByCreate2.roomId),
predecessorEvent(room2.roomId, replacedByDynamicPredecessor2.roomId),
],
{},
{ addToState: true },
);
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {});
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {});
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {
addToState: true,
});
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {
addToState: true,
});
replacedByDynamicPredecessor1.addLiveEvents(
[tombstoneEvent(room1.roomId, replacedByDynamicPredecessor1.roomId)],
{},
{ addToState: true },
);
replacedByDynamicPredecessor2.addLiveEvents(
[tombstoneEvent(room2.roomId, replacedByDynamicPredecessor2.roomId)],
{},
{ addToState: true },
);
return {
@@ -2854,10 +2858,10 @@ describe("MatrixClient", function () {
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
client.store = new StubStore();
client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2];
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], {});
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], {});
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], {});
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], {});
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
// When we ask for the visible rooms
const rooms = client.getVisibleRooms();
@@ -2937,15 +2941,15 @@ describe("MatrixClient", function () {
const room4 = new Room("room4", client, "@michonne:hawthorne.example.com");
if (creates) {
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]);
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]);
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]);
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)], { addToState: true });
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)], { addToState: true });
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)], { addToState: true });
}
if (tombstones) {
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {});
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {});
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {});
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], { addToState: true });
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], { addToState: true });
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], { addToState: true });
}
mocked(store.getRoom).mockImplementation((roomId: string) => {
@@ -2980,17 +2984,17 @@ describe("MatrixClient", function () {
const dynRoom4 = new Room("dynRoom4", client, "@rick:grimes.example.com");
const dynRoom5 = new Room("dynRoom5", client, "@rick:grimes.example.com");
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], {});
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)]);
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], {});
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)]);
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], {});
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)]);
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], {});
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)]);
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
mocked(store.getRoom)
.mockClear()
+164 -14
View File
@@ -38,6 +38,8 @@ const membershipTemplate: CallMembershipData = {
const mockFocus = { type: "mock" };
const textEncoder = new TextEncoder();
describe("MatrixRTCSession", () => {
let client: MatrixClient;
let sess: MatrixRTCSession | undefined;
@@ -467,19 +469,109 @@ describe("MatrixRTCSession", () => {
jest.useRealTimers();
});
it("uses membershipExpiryTimeout from join config", async () => {
const realSetTimeout = setTimeout;
jest.useFakeTimers();
sess!.joinRoomSession([mockFocus], mockFocus, { membershipExpiryTimeout: 60000 });
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 60000,
expires_ts: Date.now() + 60000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
},
"@alice:example.org",
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
jest.useRealTimers();
});
describe("non-legacy calls", () => {
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
async function testJoin(useOwnedStateEvents: boolean): Promise<void> {
const realSetTimeout = setTimeout;
if (useOwnedStateEvents) {
mockRoom.getVersion = jest.fn().mockReturnValue("org.matrix.msc3779.default");
mockRoom.getVersion = jest.fn().mockReturnValue("org.matrix.msc3757.default");
}
jest.useFakeTimers();
sess!.joinRoomSession([activeFocusConfig], activeFocus, { useLegacyMemberEvents: false });
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
// preparing the delayed disconnect should handle the delay being too long
const sendDelayedStateExceedAttempt = new Promise<void>((resolve) => {
const error = new MatrixError({
"errcode": "M_UNKNOWN",
"org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
"org.matrix.msc4140.max_delay": 7500,
});
sendDelayedStateMock.mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
});
// preparing the delayed disconnect should handle ratelimiting
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
sendDelayedStateMock.mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
});
// setting the membership state should handle ratelimiting (also with a retry-after value)
const sendStateEventAttempt = new Promise<void>((resolve) => {
const error = new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
429,
undefined,
undefined,
new Headers({ "Retry-After": "1" }),
);
sendStateEventMock.mockImplementationOnce(() => {
resolve();
return Promise.reject(error);
});
});
// needed to advance the mock timers properly
const scheduledDelayDisconnection = new Promise<void>((resolve) => {
const originalFn: () => void = (sess as any).scheduleDelayDisconnection;
(sess as any).scheduleDelayDisconnection = jest.fn(() => {
originalFn.call(sess);
resolve();
});
});
sess!.joinRoomSession([activeFocusConfig], activeFocus, {
useLegacyMemberEvents: false,
membershipServerSideExpiryTimeout: 9000,
});
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 9000);
await sendDelayedStateExceedAttempt.then(); // needed to resolve after the send attempt catches
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 7500);
await sendDelayedStateAttempt;
jest.advanceTimersByTime(5000);
await sendStateEventAttempt.then(); // needed to resolve after resendIfRateLimited catches
jest.advanceTimersByTime(1000);
await sentStateEvent;
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
@@ -493,9 +585,10 @@ describe("MatrixRTCSession", () => {
} satisfies SessionMembershipData,
`${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`,
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
await sentDelayedState;
// should have prepared the heartbeat to keep delaying the leave event while still connected
await scheduledDelayDisconnection;
// should have tried updating the delayed leave to test that it wasn't replaced by own state
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
// should update delayed disconnect
@@ -773,7 +866,64 @@ describe("MatrixRTCSession", () => {
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
});
it("Re-sends key if a new member joins", async () => {
it("re-sends key if a new member joins even if a key rotation is in progress", async () => {
jest.useFakeTimers();
try {
// session with two members
const member2 = Object.assign({}, membershipTemplate, {
device_id: "BBBBBBB",
});
const mockRoom = makeMockRoom([membershipTemplate, member2]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
// joining will trigger an initial key send
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
sess.joinRoomSession([mockFocus], mockFocus, {
manageMediaKeys: true,
updateEncryptionKeyThrottle: 1000,
makeKeyDelay: 3000,
});
await keysSentPromise1;
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
// member2 leaves triggering key rotation
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
sess.onMembershipUpdate();
// member2 re-joins which should trigger an immediate re-send
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
sess.onMembershipUpdate();
// but, that immediate resend is throttled so we need to wait a bit
jest.advanceTimersByTime(1000);
const { keys } = await keysSentPromise2;
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
// key index should still be the original: 0
expect(keys[0].index).toEqual(0);
// check that the key rotation actually happens
const keysSentPromise3 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
jest.advanceTimersByTime(2000);
const { keys: rotatedKeys } = await keysSentPromise3;
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(3);
// key index should now be the rotated one: 1
expect(rotatedKeys[0].index).toEqual(1);
} finally {
jest.useRealTimers();
}
});
it("re-sends key if a new member joins", async () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
@@ -1197,7 +1347,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
0,
"@bob:example.org:bobsphone",
);
@@ -1229,7 +1379,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
4,
"@bob:example.org:bobsphone",
);
@@ -1261,7 +1411,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
0,
"@bob:example.org:bobsphone",
);
@@ -1288,12 +1438,12 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(2);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
0,
"@bob:example.org:bobsphone",
);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
4,
"@bob:example.org:bobsphone",
);
@@ -1341,7 +1491,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("newer key", "utf-8"),
textEncoder.encode("newer key"),
0,
"@bob:example.org:bobsphone",
);
@@ -1389,7 +1539,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("second key", "utf-8"),
textEncoder.encode("second key"),
0,
"@bob:example.org:bobsphone",
);
+3 -3
View File
@@ -70,7 +70,7 @@ describe("Beacon", () => {
const advanceDateAndTime = (ms: number) => {
// bc liveness check uses Date.now we have to advance this mock
jest.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
jest.spyOn(globalThis.Date, "now").mockReturnValue(Date.now() + ms);
// then advance time for the interval by the same amount
jest.advanceTimersByTime(ms);
};
@@ -108,11 +108,11 @@ describe("Beacon", () => {
);
// back to 'now'
jest.spyOn(global.Date, "now").mockReturnValue(now);
jest.spyOn(globalThis.Date, "now").mockReturnValue(now);
});
afterAll(() => {
jest.spyOn(global.Date, "now").mockRestore();
jest.spyOn(globalThis.Date, "now").mockRestore();
});
it("creates beacon from event", () => {
+6 -6
View File
@@ -99,7 +99,7 @@ describe("MatrixEvent", () => {
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const ev = createEvent("$event1:server");
await room.addLiveEvents([ev]);
await room.addLiveEvents([ev], { addToState: false });
await room.createThreadsTimelineSets();
expect(ev.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
@@ -120,7 +120,7 @@ describe("MatrixEvent", () => {
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]);
await room.addLiveEvents([threadRoot, ev], { addToState: false });
await room.createThreadsTimelineSets();
expect(threadRoot.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -143,7 +143,7 @@ describe("MatrixEvent", () => {
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]);
await room.addLiveEvents([threadRoot, ev], { addToState: false });
await room.createThreadsTimelineSets();
expect(ev.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -167,7 +167,7 @@ describe("MatrixEvent", () => {
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const reaction = createReactionEvent("$reaction:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, reaction]);
await room.addLiveEvents([threadRoot, ev, reaction], { addToState: false });
await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -191,7 +191,7 @@ describe("MatrixEvent", () => {
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const edit = createEditEvent("$edit:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, edit]);
await room.addLiveEvents([threadRoot, ev, edit], { addToState: false });
await room.createThreadsTimelineSets();
expect(edit.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -217,7 +217,7 @@ describe("MatrixEvent", () => {
const reply2 = createReplyEvent("$reply2:server", reply1.getId()!);
const reaction = createReactionEvent("$reaction:server", reply2.getId()!);
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction]);
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction], { addToState: false });
await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
+22 -22
View File
@@ -36,7 +36,7 @@ describe("RoomReceipts", () => {
// Given there are no receipts in the room
const room = createRoom();
const [event] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// When I ask about any event, then it is unread
expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(false);
@@ -46,7 +46,7 @@ describe("RoomReceipts", () => {
// Given there are no receipts in the room
const room = createRoom();
const [event] = createEventSentBy(readerId);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// When I ask about an event I sent, it is read (because a synthetic
// receipt was created and stored in RoomReceipts)
@@ -57,7 +57,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user
@@ -72,7 +72,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [event1, event1Id] = createEvent();
const [event2] = createEvent();
room.addLiveEvents([event1, event2]);
room.addLiveEvents([event1, event2], { addToState: false });
// When we receive a receipt for the later event
room.addReceipt(createReceipt(readerId, event2));
@@ -86,7 +86,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [oldEvent, oldEventId] = createEvent();
const [liveEvent] = createEvent();
room.addLiveEvents([liveEvent]);
room.addLiveEvents([liveEvent], { addToState: false });
createOldTimeline(room, [oldEvent]);
// When we receive a receipt for the live event
@@ -120,7 +120,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [event1] = createEvent();
const [event2, event2Id] = createEvent();
room.addLiveEvents([event1, event2]);
room.addLiveEvents([event1, event2], { addToState: false });
// When we receive a receipt for the earlier event
room.addReceipt(createReceipt(readerId, event1));
@@ -133,7 +133,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user
@@ -151,7 +151,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [previousEvent] = createEvent();
const [myEvent] = createEventSentBy(readerId);
room.addLiveEvents([previousEvent, myEvent]);
room.addLiveEvents([previousEvent, myEvent], { addToState: false });
// And I just received a receipt for the previous event
room.addReceipt(createReceipt(readerId, previousEvent));
@@ -165,7 +165,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [myEvent] = createEventSentBy(readerId);
const [laterEvent] = createEvent();
room.addLiveEvents([myEvent, laterEvent]);
room.addLiveEvents([myEvent, laterEvent], { addToState: false });
// When I ask about the later event, it is unread (because it's after the synthetic receipt)
expect(room.hasUserReadEvent(readerId, laterEvent.getId()!)).toBe(false);
@@ -177,7 +177,7 @@ describe("RoomReceipts", () => {
const [event1] = createEvent();
const [event2, event2Id] = createEvent();
const [event3, event3Id] = createEvent();
room.addLiveEvents([event1, event2, event3]);
room.addLiveEvents([event1, event2, event3], { addToState: false });
// When we receive receipts for the older events out of order
room.addReceipt(createReceipt(readerId, event2));
@@ -192,7 +192,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user
@@ -208,7 +208,7 @@ describe("RoomReceipts", () => {
const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event]);
room.addLiveEvents([root, event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event on this thread
@@ -225,7 +225,7 @@ describe("RoomReceipts", () => {
const [event1, event1Id] = createThreadedEvent(root);
const [event2] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2]);
room.addLiveEvents([root, event1, event2], { addToState: false });
// When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
@@ -241,7 +241,7 @@ describe("RoomReceipts", () => {
const [event1] = createThreadedEvent(root);
const [event2, event2Id] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2]);
room.addLiveEvents([root, event1, event2], { addToState: false });
// When we receive a receipt for the earlier event
room.addReceipt(createThreadedReceipt(readerId, event1, rootId));
@@ -256,7 +256,7 @@ describe("RoomReceipts", () => {
const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event]);
room.addLiveEvents([root, event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user
@@ -278,7 +278,7 @@ describe("RoomReceipts", () => {
const [thread2] = createThreadedEvent(root2);
setupThread(room, root1);
setupThread(room, root2);
room.addLiveEvents([root1, root2, thread1, thread2]);
room.addLiveEvents([root1, root2, thread1, thread2], { addToState: false });
// When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, thread2, root2.getId()!));
@@ -295,7 +295,7 @@ describe("RoomReceipts", () => {
const [event2, event2Id] = createThreadedEvent(root);
const [event3, event3Id] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2, event3]);
room.addLiveEvents([root, event1, event2, event3], { addToState: false });
// When we receive receipts for the older events out of order
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
@@ -329,7 +329,7 @@ describe("RoomReceipts", () => {
const [thread2b, thread2bId] = createThreadedEvent(main2);
setupThread(room, main1);
setupThread(room, main2);
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b]);
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b], { addToState: false });
// And the timestamps on the events are consistent with the order above
main1.event.origin_server_ts = 1;
@@ -377,7 +377,7 @@ describe("RoomReceipts", () => {
// Add the event to the room
// The receipt is removed from the dangling state
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
@@ -398,7 +398,7 @@ describe("RoomReceipts", () => {
// Add the events to the room
// The receipt is removed from the dangling state
room.addLiveEvents([root, event]);
room.addLiveEvents([root, event], { addToState: false });
// Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
@@ -418,7 +418,7 @@ describe("RoomReceipts", () => {
// Add the event to the room
// The two receipts should be processed
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// Then the event is read
// We expect that the receipt of `otherUserId` didn't replace/erase the receipt of `readerId`
@@ -528,7 +528,7 @@ function createThreadedReceipt(userId: string, referencedEvent: MatrixEvent, thr
*/
function createOldTimeline(room: Room, events: MatrixEvent[]) {
const oldTimeline = room.getUnfilteredTimelineSet().addTimeline();
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, oldTimeline);
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, false, oldTimeline);
}
/**
+1 -1
View File
@@ -801,7 +801,7 @@ async function createThread(client: MatrixClient, user: string, roomId: string):
// Ensure the root is in the room timeline
root.setThreadId(root.getId());
await room.addLiveEvents([root]);
await room.addLiveEvents([root], { addToState: false });
// Create the thread and wait for it to be initialised
const thread = room.createThread(root.getId()!, root, [], false);
+1 -1
View File
@@ -106,7 +106,7 @@ describe("fixNotificationCountOnDecryption", () => {
mockClient,
);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
THREAD_ID = event.getId()!;
threadEvent = mkEvent({
+1 -1
View File
@@ -56,7 +56,7 @@ describe("oidc authorization", () => {
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
global.TextEncoder = TextEncoder;
globalThis.TextEncoder = TextEncoder;
});
beforeEach(() => {
+1 -1
View File
@@ -35,7 +35,7 @@ describe("Pushers", () => {
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
});
});
+2 -2
View File
@@ -82,7 +82,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store,
});
});
@@ -256,7 +256,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
client = new MatrixClient({
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
store,
});
});
+1 -1
View File
@@ -54,7 +54,7 @@ describe("Read receipt", () => {
userId: "@user:server",
baseUrl: "https://my.home.server",
accessToken: "my.access.token",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
});
client.isGuest = () => false;
client.supportsThreads = () => true;
+2 -2
View File
@@ -53,8 +53,8 @@ describe("realtime-callbacks", function () {
it("should set 'this' to the global object", function () {
let passed = false;
const callback = function (this: typeof global) {
expect(this).toBe(global); // eslint-disable-line @typescript-eslint/no-invalid-this
const callback = function (this: typeof globalThis) {
expect(this).toBe(globalThis); // eslint-disable-line @typescript-eslint/no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line @typescript-eslint/no-invalid-this
passed = true;
};
+4 -4
View File
@@ -198,8 +198,8 @@ describe("Relations", function () {
});
const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(targetEvent);
timelineSet.addLiveEvent(relationEvent);
timelineSet.addLiveEvent(targetEvent, { addToState: false });
timelineSet.addLiveEvent(relationEvent, { addToState: false });
await relationsCreated;
}
@@ -212,8 +212,8 @@ describe("Relations", function () {
});
const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(relationEvent);
timelineSet.addLiveEvent(targetEvent);
timelineSet.addLiveEvent(relationEvent, { addToState: false });
timelineSet.addLiveEvent(targetEvent, { addToState: false });
await relationsCreated;
}
+37 -1
View File
@@ -486,7 +486,43 @@ describe("RoomMember", function () {
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alíce"); // it should disambig.
expect(member.name).toEqual("Alíce (@alice:bar)"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
it("should disambiguate a user when their displayname looks like an MXID which isn't theirs", function () {
const joinEvent = utils.mkMembership({
event: true,
mship: KnownMembership.Join,
user: userA,
room: roomId,
name: "@clarissa\u0a83bar",
});
const roomState = {
getStateEvents: function (type: string) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true,
mship: KnownMembership.Join,
room: roomId,
user: userC,
name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function (displayName: string) {
return [userA, userC];
},
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).toEqual("@clarissabar (@alice:bar)"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
-55
View File
@@ -1122,59 +1122,4 @@ describe("RoomState", function () {
).toBeFalsy();
});
});
describe("skipWrongOrderRoomStateInserts", () => {
const idNow = "now";
const idBefore = "before";
const endState = new RoomState(roomId);
const startState = new RoomState(roomId, undefined, true);
let onRoomStateEvent: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
const evNow = new MatrixEvent({
type: "m.call.member",
room_id: roomId,
state_key: "@user:example.org",
content: {},
});
evNow.event.unsigned = { replaces_state: idBefore };
evNow.event.event_id = idNow;
const evBefore = new MatrixEvent({
type: "m.call.member",
room_id: roomId,
state_key: "@user:example.org",
content: {},
});
const updatedStateWithBefore = jest.fn();
const updatedStateWithNow = jest.fn();
beforeEach(() => {
evBefore.event.event_id = idBefore;
onRoomStateEvent = (event, _state, _lastState) => {
if (event.event.event_id === idNow) {
updatedStateWithNow();
} else if (event.event.event_id === idBefore) {
updatedStateWithBefore();
}
};
endState.on(RoomStateEvent.Events, onRoomStateEvent);
startState.on(RoomStateEvent.Events, onRoomStateEvent);
});
afterEach(() => {
endState.off(RoomStateEvent.Events, onRoomStateEvent);
startState.off(RoomStateEvent.Events, onRoomStateEvent);
updatedStateWithNow.mockReset();
updatedStateWithBefore.mockReset();
});
it("should skip inserting state to the end of the timeline if the current endState events replaces_state id is the same as the inserted events id", () => {
endState.setStateEvents([evNow, evBefore]);
expect(updatedStateWithBefore).not.toHaveBeenCalled();
expect(updatedStateWithNow).toHaveBeenCalled();
});
it("should skip inserting state at the beginning of the timeline if the inserted events replaces_state is the event id of the current startState", () => {
startState.setStateEvents([evBefore, evNow]);
expect(updatedStateWithBefore).toHaveBeenCalled();
expect(updatedStateWithNow).not.toHaveBeenCalled();
});
});
});
+594 -437
View File
File diff suppressed because it is too large Load Diff
@@ -61,7 +61,7 @@ describe("OutgoingRequestProcessor", () => {
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
baseUrl: "https://example.com",
prefix: "/_matrix",
fetchFn: httpBackend.fetchFn as typeof global.fetch,
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
onlyData: true,
});
+4 -6
View File
@@ -3,13 +3,11 @@ import fetchMock from "fetch-mock-jest";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
import { CryptoEvent, KeyBackupSession } from "../../../src/crypto-api/index.ts";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import * as testData from "../../test-utils/test-data";
import * as TestData from "../../test-utils/test-data";
import { IKeyBackup } from "../../../src/crypto/backup";
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
import { RustBackupManager } from "../../../src/rust-crypto/backup";
import { RustBackupManager, KeyBackup } from "../../../src/rust-crypto/backup";
describe("Upload keys to backup", () => {
/** The backup manager under test */
@@ -27,7 +25,7 @@ describe("Upload keys to backup", () => {
let idGenerator = 0;
function mockBackupRequest(keyCount: number): RustSdkCryptoJs.KeysBackupRequest {
const requestBody: IKeyBackup = {
const requestBody: KeyBackup = {
rooms: {
"!room1:server": {
sessions: {},
@@ -35,7 +33,7 @@ describe("Upload keys to backup", () => {
},
};
for (let i = 0; i < keyCount; i++) {
requestBody.rooms["!room1:server"].sessions["session" + i] = {} as IKeyBackupSession;
requestBody.rooms["!room1:server"].sessions["session" + i] = {} as KeyBackupSession;
}
return {
id: "id" + idGenerator++,
+63 -9
View File
@@ -572,15 +572,37 @@ describe("RustCrypto", () => {
});
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
}),
testData.TEST_USER_ID,
);
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: {
[testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA,
},
},
});
// wait until we know about the other device
rustCrypto.onSyncCompleted({});
await rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
const toDeviceEvent = {
type: "m.key.verification.request",
content: {
from_device: "testDeviceId",
from_device: testData.TEST_DEVICE_ID,
methods: ["m.sas.v1"],
transaction_id: "testTxn",
timestamp: Date.now() - 1000,
},
sender: "@user:id",
sender: testData.TEST_USER_ID,
};
const onEvent = jest.fn();
@@ -991,23 +1013,41 @@ describe("RustCrypto", () => {
});
it.each([
[undefined, null],
["Encrypted by an unverified user.", EventShieldReason.UNVERIFIED_IDENTITY],
["Encrypted by a device not verified by its owner.", EventShieldReason.UNSIGNED_DEVICE],
[undefined, undefined, null],
[
"Encrypted by an unverified user.",
RustSdkCryptoJs.ShieldStateCode.UnverifiedIdentity,
EventShieldReason.UNVERIFIED_IDENTITY,
],
[
"Encrypted by a device not verified by its owner.",
RustSdkCryptoJs.ShieldStateCode.UnsignedDevice,
EventShieldReason.UNSIGNED_DEVICE,
],
[
"The authenticity of this encrypted message can't be guaranteed on this device.",
RustSdkCryptoJs.ShieldStateCode.AuthenticityNotGuaranteed,
EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
],
["Encrypted by an unknown or deleted device.", EventShieldReason.UNKNOWN_DEVICE],
["bloop", EventShieldReason.UNKNOWN],
])("gets the right shield reason (%s)", async (rustReason, expectedReason) => {
[
"Encrypted by an unknown or deleted device.",
RustSdkCryptoJs.ShieldStateCode.UnknownDevice,
EventShieldReason.UNKNOWN_DEVICE,
],
["Not encrypted.", RustSdkCryptoJs.ShieldStateCode.SentInClear, EventShieldReason.SENT_IN_CLEAR],
[
"Encrypted by a previously-verified user who is no longer verified.",
RustSdkCryptoJs.ShieldStateCode.VerificationViolation,
EventShieldReason.VERIFICATION_VIOLATION,
],
])("gets the right shield reason (%s)", async (rustReason, rustCode, expectedReason) => {
// suppress the warning from the unknown shield reason
jest.spyOn(console, "warn").mockImplementation(() => {});
const mockEncryptionInfo = {
shieldState: jest
.fn()
.mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, message: rustReason }),
.mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, code: rustCode, message: rustReason }),
} as unknown as RustSdkCryptoJs.EncryptionInfo;
olmMachine.getRoomEventEncryptionInfo.mockResolvedValue(mockEncryptionInfo);
@@ -1526,6 +1566,20 @@ describe("RustCrypto", () => {
failures: 1,
});
});
describe("getKeyBackupInfo", () => {
it("should return the current key backup info", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi());
await expect(rustCrypto.getKeyBackupInfo()).resolves.toStrictEqual(testData.SIGNED_BACKUP_DATA);
});
it("should return null if not available", async () => {
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi());
await expect(rustCrypto.getKeyBackupInfo()).resolves.toBeNull();
});
});
});
describe("device dehydration", () => {

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