Compare commits
414 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14b2ee2da4 | |||
| bb083222d9 | |||
| fa424c44b4 | |||
| fef093747e | |||
| 4b33892d48 | |||
| d7d771fadb | |||
| 4ee3e591bf | |||
| 668183d722 | |||
| 854dae0dc0 | |||
| 9d1aca2232 | |||
| 81569f3461 | |||
| 50783aba76 | |||
| fd01b17236 | |||
| 25e92009b7 | |||
| eb7acfb810 | |||
| ca5655bced | |||
| ef9b13e2a6 | |||
| 159cca0363 | |||
| 3879111850 | |||
| f9a5aa87e3 | |||
| ed58df040c | |||
| 0a3448d4c9 | |||
| 25c1c1ea26 | |||
| a5e67af31f | |||
| b91e80814a | |||
| d096a72605 | |||
| fb547e7b4b | |||
| 815294ca5a | |||
| 6d270b4685 | |||
| 8bc3d96f6b | |||
| b6ea6e105e | |||
| cd4e053fa5 | |||
| 727473af62 | |||
| f17f013f1e | |||
| 9f4ab0b840 | |||
| 6371e4b252 | |||
| b69929e01a | |||
| 9dc12baaa9 | |||
| 159738597d | |||
| d02205652f | |||
| 5e03add29a | |||
| eeafd7fcaa | |||
| 78a3c5372d | |||
| c0c3bc2a8c | |||
| c3ce49cabf | |||
| 5408168dfd | |||
| 61452ddc11 | |||
| d5160a5380 | |||
| 7ff4960a27 | |||
| 00f63db80f | |||
| 9bcb83a20a | |||
| dd8d8e5410 | |||
| 32b8ff8116 | |||
| 3ceadd512d | |||
| 8182180550 | |||
| aed74c5a72 | |||
| 8c259c53a6 | |||
| 4d59291538 | |||
| 80009a1b31 | |||
| 93e6c95953 | |||
| 27a5507cef | |||
| be06f6655e | |||
| 71152f33bf | |||
| bc8f67089c | |||
| acc9aa8939 | |||
| e76f627fe3 | |||
| 45b1e73842 | |||
| f3eefd2f32 | |||
| f7c053216b | |||
| 9c7739f14f | |||
| 897afe153a | |||
| a929391dcd | |||
| 45c5ee9f65 | |||
| e56aaa16c7 | |||
| da0d3d791e | |||
| ed5eb670a1 | |||
| b7fcb6e4c1 | |||
| bd775f6b61 | |||
| c2f9ad28fc | |||
| 7f33e3462e | |||
| d99363d288 | |||
| 3642b99212 | |||
| 6ec0987286 | |||
| 219eb617dc | |||
| c6f9b25046 | |||
| 5d0e2efaf3 | |||
| c7cd5570d3 | |||
| c2f6dd2ce0 | |||
| 393732aaae | |||
| d373fd8540 | |||
| 44a8a9a47a | |||
| 8a0b7ad68b | |||
| 09663302e1 | |||
| 94f83b702c | |||
| 9df27ee672 | |||
| 5739b59faa | |||
| e4425570c7 | |||
| 145cb26054 | |||
| 26d5b1cde2 | |||
| 0666d6b4e1 | |||
| 9002064f10 | |||
| 5ea1554612 | |||
| bd6547c081 | |||
| 8073f27d98 | |||
| 3bb22a9b28 | |||
| ba8bb3228d | |||
| de23c9587b | |||
| 64eb482a49 | |||
| aba7f8a0d4 | |||
| 5495153c63 | |||
| 4f0696e2a4 | |||
| 0e659d294e | |||
| e74eb4928e | |||
| 327d2fa7c8 | |||
| 028357f15f | |||
| 872ec6755e | |||
| 333d6a7bd6 | |||
| 47532de452 | |||
| 87e1049dae | |||
| 4c8e38009f | |||
| 6e3efef0c5 | |||
| fb590627bb | |||
| 24cc17c270 | |||
| 68084e8fc3 | |||
| 0c3bb1f246 | |||
| 7f42b67f68 | |||
| 9b871ac969 | |||
| 49f7972a9e | |||
| c5ae4c8c0d | |||
| 2423300acd | |||
| 6cafa175b8 | |||
| 40165942e3 | |||
| f301251ff5 | |||
| 21cd5e98c1 | |||
| db070dca57 | |||
| 8b6ff0abcb | |||
| f2157f28bb | |||
| 739b8e1f89 | |||
| bc57a3f829 | |||
| f136f6ddf7 | |||
| d428e7119a | |||
| 5532066178 | |||
| 82b51d0d46 | |||
| fb12a5a1d6 | |||
| 61ea5a7dfc | |||
| 4e032317fe | |||
| dbb2ae5c07 | |||
| c8032a214e | |||
| 6e34ca6f2d | |||
| 4a7a699623 | |||
| 774776178e | |||
| ed4078528d | |||
| 7224961e9e | |||
| 35ca07e8fb | |||
| ccffb5df2d | |||
| 0d162f66f3 | |||
| bb7a689448 | |||
| c2b464a72c | |||
| 4a75d2c92f | |||
| 899cdb0e1d | |||
| da7c6717fe | |||
| b3fedf3a4e | |||
| 3d0ebdf6f1 | |||
| 25555ec431 | |||
| 33cd424f1f | |||
| 4d0d32307e | |||
| b1a578f62e | |||
| 841b654c00 | |||
| ca0d4622b3 | |||
| bfd87a0896 | |||
| 6c59b0c22f | |||
| eff75c6525 | |||
| ee4a0b001e | |||
| 5fcd6fd744 | |||
| bf58f18b5f | |||
| 0c020b3ca4 | |||
| 2727ebb67f | |||
| 8fae4f3111 | |||
| 455b614008 | |||
| 93f4f40202 | |||
| aeade9ce58 | |||
| 4b89fb23c5 | |||
| 174439c2f0 | |||
| 43f3e10f05 | |||
| 97fcdb2830 | |||
| 31e2d8eb20 | |||
| 633a5a8848 | |||
| a5086a09b9 | |||
| c251be9ae5 | |||
| ec137cb5fb | |||
| ab4e24f115 | |||
| 2218ec4e31 | |||
| 319a8309c5 | |||
| 5af046f54f | |||
| f97a9d9762 | |||
| dc1a57a9f2 | |||
| 2d6111a04b | |||
| 710fd7859d | |||
| e340a4ceaf | |||
| 3fa44e076e | |||
| 3f9fb9c936 | |||
| 8db347a75e | |||
| 8db3343280 | |||
| 25c5a5b4ff | |||
| a696e77652 | |||
| 582a76d87c | |||
| fdfddde55a | |||
| 0ecfef2352 | |||
| 4fdece6c1c | |||
| 6f0bce8708 | |||
| d3bdeb73f5 | |||
| 942fdf5bee | |||
| dd2635dbe6 | |||
| 3d1bcb73c1 | |||
| a960e686b3 | |||
| 946774c3fb | |||
| 15edbc8067 | |||
| 1398ac24a2 | |||
| c76df4cd8f | |||
| a5e4dbf2d3 | |||
| 3768187395 | |||
| 08d0ce25f1 | |||
| 23241f18e2 | |||
| 90da67aa95 | |||
| 0bf2702149 | |||
| c7a75c8824 | |||
| 98b2b9745d | |||
| 65d5b3172c | |||
| 2f72f9e889 | |||
| 18f500a1f8 | |||
| b1df58796a | |||
| 761b3771d6 | |||
| df88edfda0 | |||
| 1dee1ba581 | |||
| b274c74a30 | |||
| b489bb15cf | |||
| dff4922a42 | |||
| dc6ad0b54c | |||
| 9769c05dc5 | |||
| dd379d3d4c | |||
| 1b884a3e52 | |||
| ddb164490e | |||
| 796135c7ce | |||
| 4cc4c01dd8 | |||
| 533b40922c | |||
| 0ae483ce27 | |||
| dbc1fa87ed | |||
| 0a3675b971 | |||
| ab3f529d29 | |||
| 607b712a07 | |||
| b6d9e49277 | |||
| 731d5943e2 | |||
| 01e7a43593 | |||
| b69a19ce7c | |||
| 8703acb533 | |||
| b59603d748 | |||
| b0cbe22f64 | |||
| 977d0322da | |||
| dd7394c14c | |||
| f2d082064e | |||
| 2731e20893 | |||
| 502a513b5b | |||
| 3ac47e71cd | |||
| 7c1e25e713 | |||
| 2d90ad95f1 | |||
| cd9794471f | |||
| b2d3ab8bc1 | |||
| d8b70ef83b | |||
| a67fb1fb8d | |||
| ddd6e77cde | |||
| 2e9f5b6033 | |||
| fd949fe486 | |||
| 7b3aed8a47 | |||
| b84a73c7cc | |||
| a03cf054a8 | |||
| b3d217717a | |||
| 3e6fe5f914 | |||
| a213d177f9 | |||
| e885ecf08d | |||
| d1d9aba745 | |||
| 52bcc2c955 | |||
| 1994806c72 | |||
| c4d1fd2c67 | |||
| 7ad8288525 | |||
| 31a42964e6 | |||
| 2b1d37813c | |||
| e6fd0c58ff | |||
| 41d70d0b5d | |||
| a08a2737e1 | |||
| dbe441de33 | |||
| 9f3ca71495 | |||
| ef97df8ed0 | |||
| 7f74fcc9f7 | |||
| e39644ad08 | |||
| df0f0074b4 | |||
| 29fbed5603 | |||
| 5ee6fc196b | |||
| 961e32a3bb | |||
| 25f0418fce | |||
| 0ce751c462 | |||
| b7b3588cb8 | |||
| 72846c713d | |||
| 7c5229b4c8 | |||
| dd08388397 | |||
| ec1ccebcca | |||
| e9b45cc504 | |||
| 5d4df65c09 | |||
| 2706873948 | |||
| 4a9006aea6 | |||
| 43c72d5bf5 | |||
| e551b92a07 | |||
| 32f51e852b | |||
| 82aa04d894 | |||
| ff89c9ec42 | |||
| ccd825fb39 | |||
| b313eb5912 | |||
| 2b12675675 | |||
| 246788b874 | |||
| b0b80401aa | |||
| 6a0164f37f | |||
| f963d61bcb | |||
| b32619ad24 | |||
| 3d3c3ba55f | |||
| d62c658a72 | |||
| bdc4a69023 | |||
| ab892420b5 | |||
| ed607c48b0 | |||
| a8d75b81e5 | |||
| 2f1d654f14 | |||
| c4c7f94514 | |||
| 1fac06e223 | |||
| 77c118084b | |||
| 097bfe451a | |||
| b80d0091d2 | |||
| 3a33c658bb | |||
| 81e42b9531 | |||
| 7f7ecd060d | |||
| 6126ee125a | |||
| 78f718ff82 | |||
| c1c1be0c5d | |||
| 1952eaa1ff | |||
| 8851f8b07c | |||
| 15eafe34b3 | |||
| f0d48236fa | |||
| dde9d48726 | |||
| 6d046edcb2 | |||
| 2abf7ca795 | |||
| 2b46579bd8 | |||
| 6d42ed338e | |||
| ef080c25f9 | |||
| c8d7b458b2 | |||
| f1ba8a8775 | |||
| d21adf568a | |||
| dea184e9ec | |||
| e119bf9040 | |||
| c7f982e190 | |||
| 2e2dd628c1 | |||
| 5ac5a8a799 | |||
| 7d75ab417a | |||
| 3ca81e409a | |||
| 764fdb1d30 | |||
| c2d25d9377 | |||
| c4e1e0723e | |||
| 56b24c0bdc | |||
| aa7a709e3f | |||
| c57c47319e | |||
| 812d0aaef6 | |||
| 61e07633df | |||
| c7dbd6e33b | |||
| 556494b8f0 | |||
| bf3b4e81b2 | |||
| 2710600389 | |||
| ca168c494b | |||
| 759f5ed3eb | |||
| 53deedd2d6 | |||
| 4fbb9f92a7 | |||
| 1a58ce4649 | |||
| 32509d1fd1 | |||
| 11c9e39e5a | |||
| 0ce944f3da | |||
| 38e04c8fb0 | |||
| 1fcbc6ebeb | |||
| aa79236ce2 | |||
| f8f1bf3837 | |||
| 946cf4f359 | |||
| c077201f2a | |||
| 53f2ad41d6 | |||
| be15a709c6 | |||
| 090b8079db | |||
| 06a1e1a88a | |||
| 024b62bba0 | |||
| 6bfb911cf7 | |||
| ad5da7cfa1 | |||
| e55bd1e14c | |||
| 119e859741 | |||
| b7cce93edc | |||
| e5c8c20a34 | |||
| 137379b7b7 | |||
| 4981efa54b | |||
| 6e9740d787 | |||
| 70257e0ab4 | |||
| 9baba151c6 | |||
| b4672e26ec | |||
| 940d358b0e | |||
| 161c12f5d5 | |||
| de659d6431 | |||
| 18ea8befdc | |||
| 4f9ca2c697 | |||
| 57a4dc8841 | |||
| 841c02e56d | |||
| 6ece4c3c16 | |||
| 5a3c07f91d | |||
| 3ed4b3ed50 | |||
| 9ca9bd9baf |
+28
-12
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc", "n"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
plugins: ["matrix-org", "import", "jsdoc", "n", "@vitest"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
@@ -83,16 +83,6 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"jest/no-disabled-tests": "off",
|
||||
// Used in some crypto tests.
|
||||
"jest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -147,11 +137,37 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
files: ["spec/**/*.ts"],
|
||||
extends: ["plugin:@vitest/legacy-recommended"],
|
||||
rules: {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"@vitest/no-disabled-tests": "off",
|
||||
// Used in some crypto tests.
|
||||
"@vitest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach"],
|
||||
},
|
||||
],
|
||||
"@vitest/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
assertFunctionNames: [
|
||||
"expect",
|
||||
"expectDevices",
|
||||
"assert.isTrue",
|
||||
"assert.isFalse",
|
||||
"passwordTest",
|
||||
"compareHeaders",
|
||||
"doTest",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
* @matrix-org/element-web-reviewers
|
||||
/.github/workflows/** @matrix-org/element-web-team
|
||||
/package.json @matrix-org/element-web-team
|
||||
/yarn.lock @matrix-org/element-web-team
|
||||
/pnpm-lock.yaml @matrix-org/element-web-team
|
||||
/scripts/** @matrix-org/element-web-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@d22998fda4c1407f60d1ab48cd6fe67f360f34de # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # 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@d22998fda4c1407f60d1ab48cd6fe67f360f34de # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@d22998fda4c1407f60d1ab48cd6fe67f360f34de # v1
|
||||
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -41,3 +41,6 @@
|
||||
- name: "Z-Flaky-Test"
|
||||
description: "A test is raising false alarms"
|
||||
color: "ededed"
|
||||
- name: "Z-Skip-Coverage"
|
||||
description: "Skip SonarQube coverage for this PR"
|
||||
color: "ededed"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
# Privilege escalation necessary to enable backporting PRs from forks
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
name: Deploy documentation PR preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
# Privilege escalation necessary to publish to Netlify
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Static Analysis"]
|
||||
types:
|
||||
- completed
|
||||
@@ -15,7 +17,7 @@ jobs:
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
||||
# matrix-react-sdk playwright tests (with access to repo secrets)
|
||||
# element-web playwright tests (with access to repo secrets)
|
||||
|
||||
name: matrix-react-sdk End to End Tests
|
||||
name: Element Web End to End Tests
|
||||
on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
@@ -21,11 +21,12 @@ concurrency:
|
||||
jobs:
|
||||
playwright:
|
||||
name: Playwright
|
||||
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
|
||||
uses: element-hq/element-web/.github/workflows/build-and-test.yaml@develop # zizmor: ignore[unpinned-uses]
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
contents: read
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
# We only want to run the playwright tests on merge queue to prevent regressions
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
- name: Notify element-web repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
# Privilege escalation necessary access members of the review teams
|
||||
# 🚨 We must not execute any checked out code here, and be careful around use of user-controlled inputs.
|
||||
# FIXME: only `community-prs` job needs this privilege, so it should be in its own workflow file.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
@@ -15,7 +18,7 @@ jobs:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5
|
||||
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
labels: |
|
||||
@@ -35,7 +38,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
@@ -49,7 +52,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check membership
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]'
|
||||
if: github.event.pull_request.user.login != 'renovate[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
|
||||
id: teams
|
||||
with:
|
||||
@@ -60,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Add label
|
||||
if: steps.teams.outputs.isTeamMember == 'false'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
@@ -73,13 +76,15 @@ jobs:
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for X-Release-Blocker label on any open issues or PRs
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
REPO: ${{ inputs.repository }}
|
||||
with:
|
||||
|
||||
@@ -16,18 +16,20 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
|
||||
id: draft-release
|
||||
@@ -37,7 +39,7 @@ jobs:
|
||||
disable-autolabeler: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -48,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Ingest upstream changes
|
||||
if: inputs.include-changes
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
draft:
|
||||
permissions:
|
||||
contents: write
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
|
||||
@@ -12,20 +12,25 @@ on:
|
||||
description: List of dependencies to reset.
|
||||
type: string
|
||||
required: false
|
||||
dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
concurrency: ${{ github.workflow }}
|
||||
permissions: {} # Uses ELEMENT_BOT_TOKEN
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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
|
||||
persist-credentials: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -33,13 +38,14 @@ jobs:
|
||||
sparse-checkout: |
|
||||
scripts/release
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Set up git
|
||||
run: |
|
||||
@@ -53,6 +59,7 @@ jobs:
|
||||
|
||||
- name: Reset dependencies
|
||||
if: inputs.dependencies
|
||||
working-directory: ${{ inputs.dir }}
|
||||
run: |
|
||||
while IFS= read -r PACKAGE; do
|
||||
[ -z "$PACKAGE" ] && continue
|
||||
@@ -73,7 +80,7 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "Resetting $PACKAGE to develop branch..."
|
||||
yarn add "github:matrix-org/$PACKAGE#develop"
|
||||
pnpm add "github:matrix-org/$PACKAGE#develop"
|
||||
git add -u
|
||||
git commit -m "Reset $PACKAGE back to develop branch"
|
||||
done <<< "$DEPENDENCIES"
|
||||
|
||||
@@ -4,8 +4,6 @@ on:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
NPM_TOKEN:
|
||||
required: false
|
||||
GPG_PASSPHRASE:
|
||||
required: false
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -28,12 +26,21 @@ on:
|
||||
description: |
|
||||
The path to the asset you want to upload, if any. You can use glob patterns here.
|
||||
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
|
||||
Relative to `dir`.
|
||||
type: string
|
||||
required: false
|
||||
expected-asset-count:
|
||||
description: The number of expected assets, including signatures, excluding generated zip & tarball.
|
||||
type: number
|
||||
required: false
|
||||
dist-dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
version-dirs:
|
||||
description: Directories in which to update package.json `version` field
|
||||
type: string
|
||||
required: false
|
||||
outputs:
|
||||
npm-id:
|
||||
description: "The npm package@version string we published"
|
||||
@@ -45,7 +52,7 @@ jobs:
|
||||
permissions:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
|
||||
release:
|
||||
name: Release
|
||||
@@ -58,7 +65,7 @@ jobs:
|
||||
- name: Load GPG key
|
||||
id: gpg
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
@@ -66,22 +73,23 @@ jobs:
|
||||
|
||||
- name: Get draft release
|
||||
id: draft-release
|
||||
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
|
||||
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
draft: true
|
||||
latest: true
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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
|
||||
persist-credentials: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -92,6 +100,7 @@ jobs:
|
||||
|
||||
- name: Prepare variables
|
||||
id: prepare
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: |
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
@@ -106,7 +115,7 @@ jobs:
|
||||
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Check version number not in use
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
const { VERSION } = process.env;
|
||||
@@ -125,15 +134,17 @@ jobs:
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: package.json
|
||||
cache: "pnpm"
|
||||
node-version-file: ${{ inputs.dist-dir }}/package.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Handle develop dependencies
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: |
|
||||
ret=0
|
||||
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
|
||||
@@ -142,15 +153,19 @@ jobs:
|
||||
VERSION=${dep[1]}
|
||||
|
||||
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
|
||||
yarn upgrade "$PACKAGE@$VERSION" --exact
|
||||
pnpm add "$PACKAGE@$VERSION" --save-exact
|
||||
git add -u
|
||||
git commit -m "Keep $PACKAGE at $VERSION"
|
||||
done
|
||||
|
||||
- name: Bump package.json version
|
||||
- name: Bump package.json versions
|
||||
run: |
|
||||
yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
git add package.json
|
||||
for DIR in $DIRS; do
|
||||
pnpm version -C "$DIR" --no-git-tag-version "${VERSION#v}"
|
||||
git add "$DIR"/package.json
|
||||
done
|
||||
env:
|
||||
DIRS: ${{ inputs.version-dirs || inputs.dist-dir }}
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.final
|
||||
@@ -177,7 +192,8 @@ jobs:
|
||||
|
||||
- name: Build assets
|
||||
if: steps.prepare.outputs.has-dist-script == '1'
|
||||
run: DIST_VERSION="$VERSION" yarn dist
|
||||
working-directory: ${{ inputs.dist-dir }}
|
||||
run: DIST_VERSION="$VERSION" pnpm dist
|
||||
|
||||
- name: Upload release assets & signatures
|
||||
if: inputs.asset-path
|
||||
@@ -185,7 +201,7 @@ jobs:
|
||||
with:
|
||||
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
|
||||
upload-url: ${{ steps.draft-release.outputs.upload_url }}
|
||||
asset-path: ${{ inputs.asset-path }}
|
||||
asset-path: ${{ inputs.dist-dir }}/${{ inputs.asset-path }}
|
||||
|
||||
- name: Create signed tag
|
||||
if: inputs.gpg-fingerprint
|
||||
@@ -218,7 +234,7 @@ jobs:
|
||||
|
||||
- name: Validate release has expected assets
|
||||
if: inputs.expected-asset-count
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
|
||||
@@ -246,7 +262,7 @@ jobs:
|
||||
git push origin master
|
||||
|
||||
- name: Publish release
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
env:
|
||||
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
||||
FINAL: ${{ inputs.final }}
|
||||
@@ -278,12 +294,12 @@ jobs:
|
||||
name: Publish to npm
|
||||
needs: release
|
||||
if: inputs.npm
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
dir: ${{ inputs.dist-dir }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
post-release:
|
||||
name: Post release steps
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
name: Publish to npm
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
inputs:
|
||||
dir:
|
||||
description: The directory to release
|
||||
type: string
|
||||
default: "."
|
||||
outputs:
|
||||
id:
|
||||
description: "The npm package@version string we published"
|
||||
@@ -20,32 +22,32 @@ jobs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: staging
|
||||
persist-credentials: false
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version-file: package.json
|
||||
node-version-file: ${{ inputs.dir }}/package.json
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
working-directory: ${{ inputs.dir }}
|
||||
run: |
|
||||
npm publish --provenance --access public --tag next
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
release=$(jq -r '"\(.name)@\(.version)"' package.json)
|
||||
echo "id=$release" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- 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
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.id }}
|
||||
TAG: ${{ contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest' }}
|
||||
|
||||
@@ -24,7 +24,7 @@ concurrency: ${{ github.workflow }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
release:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop # zizmor: ignore[unpinned-uses,secrets-inherit]
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
@@ -41,28 +41,32 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
repo:
|
||||
- element-hq/element-web
|
||||
include:
|
||||
- repo: element-hq/element-web
|
||||
path: apps/web
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
env:
|
||||
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
|
||||
DIR: ${{ matrix.path }}
|
||||
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
|
||||
pnpm add -C "$DIR" "$DEPENDENCY" --save-exact
|
||||
git add "$DIR"/package.json pnpm-lock.yaml
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
@@ -73,22 +77,25 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 📖 Generate docs
|
||||
run: yarn gendoc
|
||||
run: pnpm gendoc
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
|
||||
with:
|
||||
path: _docs
|
||||
|
||||
@@ -106,4 +113,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
|
||||
|
||||
@@ -12,7 +12,11 @@ on:
|
||||
sharded:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
|
||||
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
|
||||
version-pkg-json-dir:
|
||||
type: string
|
||||
default: "."
|
||||
description: "Relative path of the directory containing package.json with the `version` to use."
|
||||
permissions: {}
|
||||
jobs:
|
||||
sonarqube:
|
||||
@@ -27,7 +31,7 @@ jobs:
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
|
||||
- uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -36,14 +40,15 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
persist-credentials: false
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -51,14 +56,13 @@ jobs:
|
||||
name: coverage
|
||||
path: coverage
|
||||
- name: 📥 Download sharded artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
if: inputs.sharded
|
||||
with:
|
||||
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
|
||||
@@ -75,19 +79,20 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@820f7c2e9e94ba9e35add0f739691e5c7e23fa25 # v4.0
|
||||
uses: matrix-org/sonarcloud-workflow-action@13968a27c924fa19b1dacbce6ca3ff217daa775b
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
skip_checkout: true
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
version_cmd: "cat package.json | jq -r .version"
|
||||
skip_coverage_label: Z-Skip-Coverage
|
||||
version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
- uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
# Privilege escalation necessary to call upon SonarCloud
|
||||
# 🚨 We must not execute any checked out code here.
|
||||
workflow_run: # zizmor: ignore[dangerous-triggers]
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
@@ -16,7 +18,7 @@ jobs:
|
||||
actions: read
|
||||
statuses: write
|
||||
id-token: write # sonar
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -14,58 +14,61 @@ jobs:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
run: "pnpm run lint:js"
|
||||
|
||||
node_example_lint:
|
||||
name: "Node.js example"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Build Types
|
||||
run: "yarn build:types"
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
cache: "npm"
|
||||
node-version-file: "examples/node/package.json"
|
||||
# cache-dependency-path: '**/package-lock.json'
|
||||
run: "pnpm build:types"
|
||||
|
||||
- name: Install Example Deps
|
||||
run: "npm install"
|
||||
@@ -82,39 +85,50 @@ jobs:
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn lint:workflows"
|
||||
run: "pnpm lint:workflows"
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
@@ -125,31 +139,36 @@ jobs:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run linter
|
||||
run: "yarn run lint:knip"
|
||||
run: "pnpm 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
@@ -159,15 +178,22 @@ jobs:
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
working-directory: apps/web
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
# Hook for branch protection to skip downstream typechecking outside of merge queues
|
||||
downstream:
|
||||
name: Downstream Typescript Syntax Check
|
||||
# Workflow consolidation job
|
||||
done:
|
||||
needs:
|
||||
- ts_lint
|
||||
- js_lint
|
||||
- node_example_lint
|
||||
- workflow_lint
|
||||
- docs
|
||||
- analyse_dead_code
|
||||
- element-web
|
||||
name: Static Analysis
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- element-web
|
||||
steps:
|
||||
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
|
||||
- if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
sync-labels:
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
|
||||
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
|
||||
with:
|
||||
LABELS: |
|
||||
element-hq/element-meta
|
||||
|
||||
+26
-24
@@ -12,8 +12,8 @@ env:
|
||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
|
||||
permissions: {} # No permissions required
|
||||
jobs:
|
||||
jest:
|
||||
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||
test:
|
||||
name: "Vitest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
@@ -22,17 +22,20 @@ jobs:
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
@@ -40,24 +43,23 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--ci \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
pnpm test \
|
||||
--coverage=${ENABLE_COVERAGE} \
|
||||
--maxWorkers ${NUM_WORKERS} \
|
||||
./spec/${{ matrix.specs }}
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
# tell jest to use coloured output
|
||||
FORCE_COLOR: true
|
||||
SHARD: ${{ matrix.specs }}
|
||||
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${{ steps.setupNode.outputs.node-version }}-${{ matrix.specs }}.lcov.info
|
||||
run: mv coverage/lcov.info coverage/${NODE_VERSION}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
NODE_VERSION: ${{ steps.setupNode.outputs.node-version }}
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
||||
path: |
|
||||
@@ -65,19 +67,19 @@ jobs:
|
||||
!coverage/lcov-report
|
||||
|
||||
# Dummy completion job to simplify branch protections
|
||||
jest-complete:
|
||||
name: Jest tests
|
||||
needs: jest
|
||||
complete:
|
||||
name: Tests
|
||||
needs: test
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
|
||||
- if: needs.test.result != 'skipped' && needs.test.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
element-web:
|
||||
name: Downstream test element-web
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/tests.yml@develop # zizmor: ignore[unpinned-uses]
|
||||
permissions:
|
||||
statuses: write
|
||||
with:
|
||||
@@ -87,8 +89,8 @@ 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
|
||||
permissions: read-all # zizmor: ignore[excessive-permissions]
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
use_js_sdk: "."
|
||||
|
||||
@@ -116,7 +118,7 @@ jobs:
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63
|
||||
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
automate-project-columns-next:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||
with:
|
||||
project-url: https://github.com/orgs/element-hq/projects/120
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -6,6 +6,6 @@ on:
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
call-triage-labelled:
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
|
||||
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@6339bcda15c71d209303b18a06a9b1c021220bf9
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
|
||||
with:
|
||||
operations-per-run: 250
|
||||
days-before-issue-stale: -1
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
/.npmrc
|
||||
/*.log
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
|
||||
+298
@@ -1,3 +1,301 @@
|
||||
Changes in [41.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.3.0) (2026-04-07)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Rotate the current room key when we see a member leave ([#5231](https://github.com/matrix-org/matrix-js-sdk/pull/5231)). Contributed by @kaylendog.
|
||||
|
||||
|
||||
Changes in [41.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.2.0) (2026-03-24)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Only share history if room history visibility is shared ([#5216](https://github.com/matrix-org/matrix-js-sdk/pull/5216)). Contributed by @kaylendog.
|
||||
* History sharing: resume key-bundle import on restart ([#5214](https://github.com/matrix-org/matrix-js-sdk/pull/5214)). Contributed by @richvdh.
|
||||
* Move `CryptoApi.shareRoomHistoryWithUser` to `CryptoBackend` ([#5218](https://github.com/matrix-org/matrix-js-sdk/pull/5218)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [41.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.1.0) (2026-03-10)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Throw a specific error when the backup decryption key does not match the public backup ([#5202](https://github.com/matrix-org/matrix-js-sdk/pull/5202)). Contributed by @andybalaam.
|
||||
* Update getUrlPreview to use /\_matrix/client/v1/media/preview\_url ([#5191](https://github.com/matrix-org/matrix-js-sdk/pull/5191)). Contributed by @Half-Shot.
|
||||
|
||||
|
||||
Changes in [41.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.0.0) (2026-02-24)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Download room keys from backup prior to buliding historic room key bundles ([#5171](https://github.com/matrix-org/matrix-js-sdk/pull/5171)). Contributed by @kaylendog.
|
||||
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
|
||||
* Add logging on MSC4108 DELETE request ([#5140](https://github.com/matrix-org/matrix-js-sdk/pull/5140)). Contributed by @reivilibre.
|
||||
* Add `m.invite_permission_config` account data type ([#5183](https://github.com/matrix-org/matrix-js-sdk/pull/5183)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* fix(relations): prevent stale m.replace from overriding newer edits ([#5192](https://github.com/matrix-org/matrix-js-sdk/pull/5192)). Contributed by @basnijholt.
|
||||
* Fix reactive display name disambiguation ([#5135](https://github.com/matrix-org/matrix-js-sdk/pull/5135)). Contributed by @aditya-cherukuru.
|
||||
* Fix empty string to room compatibility trick to only apply to m.call ([#5172](https://github.com/matrix-org/matrix-js-sdk/pull/5172)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [40.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.2.0) (2026-02-10)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* [MatrixRTC] Remove sending of deprecated `notify` event (we now use `m.rtc.notification`) ([#5167](https://github.com/matrix-org/matrix-js-sdk/pull/5167)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Use stable /auth\_metadata endpoint where homeserver supports v1.15 ([#5174](https://github.com/matrix-org/matrix-js-sdk/pull/5174)). Contributed by @hughns.
|
||||
* Support additional\_creators in upgradeRoom (MSC4289) ([#5173](https://github.com/matrix-org/matrix-js-sdk/pull/5173)). Contributed by @andybalaam.
|
||||
* [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId ([#5166](https://github.com/matrix-org/matrix-js-sdk/pull/5166)). Contributed by @toger5.
|
||||
* [MatrixRTC] Do not send the `livekit_alias` in sticky events ([#5165](https://github.com/matrix-org/matrix-js-sdk/pull/5165)). Contributed by @toger5.
|
||||
* Improve startup performance by using `promise.all` when processing rooms from sync ([#5095](https://github.com/matrix-org/matrix-js-sdk/pull/5095)). Contributed by @MidhunSureshR.
|
||||
* Add OAuthGrantType enum for OAuth 2.0 API grant types ([#5161](https://github.com/matrix-org/matrix-js-sdk/pull/5161)). Contributed by @hughns.
|
||||
* Add support for stable OAuth2.0 aware feature from MSC3824 ([#5159](https://github.com/matrix-org/matrix-js-sdk/pull/5159)). Contributed by @hughns.
|
||||
* Give RoomWidgetClient the ability to send and receive sticky events ([#5142](https://github.com/matrix-org/matrix-js-sdk/pull/5142)). Contributed by @robintown.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated ([#5168](https://github.com/matrix-org/matrix-js-sdk/pull/5168)). Contributed by @toger5.
|
||||
* [MatrixRTC] Fix delayId not resetting on leave ([#5156](https://github.com/matrix-org/matrix-js-sdk/pull/5156)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [40.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.1.0) (2026-01-27)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate unused `EventShieldReason` reason codes ([#5127](https://github.com/matrix-org/matrix-js-sdk/pull/5127)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add stable m.oauth UIA stage enum ([#5138](https://github.com/matrix-org/matrix-js-sdk/pull/5138)). Contributed by @hughns.
|
||||
* Add `MatrixEvent.getKeyForwardingUser` ([#5128](https://github.com/matrix-org/matrix-js-sdk/pull/5128)). Contributed by @richvdh.
|
||||
* Add types for (unstable) policy servers ([#5116](https://github.com/matrix-org/matrix-js-sdk/pull/5116)). Contributed by @turt2live.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Recalculate room name on loading members ([#5164](https://github.com/matrix-org/matrix-js-sdk/pull/5164)). Contributed by @RiotRobot.
|
||||
* Avoid rapidly retrying failed requests ([#5146](https://github.com/matrix-org/matrix-js-sdk/pull/5146)). Contributed by @andybalaam.
|
||||
* [matrixRTC] MatrixRTCSessions, add missing event reemission. ([#5144](https://github.com/matrix-org/matrix-js-sdk/pull/5144)). Contributed by @toger5.
|
||||
* Use normal base64 encoding for RTC backend identities ([#5129](https://github.com/matrix-org/matrix-js-sdk/pull/5129)). Contributed by @robintown.
|
||||
* export parseCallNotificationContent and isMyMembership from RTC types ([#5132](https://github.com/matrix-org/matrix-js-sdk/pull/5132)). Contributed by @Half-Shot.
|
||||
|
||||
|
||||
Changes in [40.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.0.0) (2026-01-13)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* MatrixRTC Pseudonymous livekit identities ([#5110](https://github.com/matrix-org/matrix-js-sdk/pull/5110)). Contributed by @toger5.
|
||||
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Mark `forwardingCurve25519KeyChain` as deprecated ([#5111](https://github.com/matrix-org/matrix-js-sdk/pull/5111)). Contributed by @richvdh.
|
||||
* Mark `IEventDecryptionResult` as deprecated ([#5112](https://github.com/matrix-org/matrix-js-sdk/pull/5112)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Implement MSC4387: M\_SAFETY error ([#5107](https://github.com/matrix-org/matrix-js-sdk/pull/5107)). Contributed by @Half-Shot.
|
||||
* Implement \_unstable\_getRTCTransports for MSC4143 ([#5104](https://github.com/matrix-org/matrix-js-sdk/pull/5104)). Contributed by @Half-Shot.
|
||||
* Use `membershipID` for session events ([#5105](https://github.com/matrix-org/matrix-js-sdk/pull/5105)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Make MatrixRTC encryption key types narrower for TS 5.9 compatibility ([#5117](https://github.com/matrix-org/matrix-js-sdk/pull/5117)). Contributed by @robintown.
|
||||
* Re-check outgoing requests after processing them ([#5109](https://github.com/matrix-org/matrix-js-sdk/pull/5109)). Contributed by @andybalaam.
|
||||
* Make token refresher init itself lazily ([#5106](https://github.com/matrix-org/matrix-js-sdk/pull/5106)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [39.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.4.0) (2025-12-16)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Import room key bundles received after invite. ([#5080](https://github.com/matrix-org/matrix-js-sdk/pull/5080)). Contributed by @kaylendog.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Allow msc4354\_sticky\_key to be optional on sticky events. ([#5073](https://github.com/matrix-org/matrix-js-sdk/pull/5073)). Contributed by @Half-Shot.
|
||||
* Handle all response fields from /context API being optional ([#5089](https://github.com/matrix-org/matrix-js-sdk/pull/5089)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [39.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.3.0) (2025-12-02)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Re-add truthy check on room name/avatar/alias events ([#5081](https://github.com/matrix-org/matrix-js-sdk/pull/5081)). Contributed by @t3chguy.
|
||||
* Fix invalid state events corrupting room objects ([#5078](https://github.com/matrix-org/matrix-js-sdk/pull/5078)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [39.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.2.0) (2025-11-18)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Delayed event management: split endpoints, no auth ([#5066](https://github.com/matrix-org/matrix-js-sdk/pull/5066)). Contributed by @AndrewFerr.
|
||||
* do not set cache in authenticated fetch ([#5020](https://github.com/matrix-org/matrix-js-sdk/pull/5020)). Contributed by @pkuzco.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix media switching during legacy calls ([#5069](https://github.com/matrix-org/matrix-js-sdk/pull/5069)). Contributed by @langleyd.
|
||||
|
||||
|
||||
Changes in [39.1.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.2) (2025-11-04)
|
||||
==================================================================================================
|
||||
Re-release of v39.1.0 to fix npm publishing workflow
|
||||
|
||||
|
||||
|
||||
Changes in [39.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.1) (2025-11-04)
|
||||
==================================================================================================
|
||||
Re-release of v39.1.0 to fix npm publishing workflow
|
||||
|
||||
Changes in [39.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.0) (2025-11-04)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* [MatrixRTC] Sticky Events support (MSC4354) ([#5017](https://github.com/matrix-org/matrix-js-sdk/pull/5017)). Contributed by @toger5.
|
||||
* Add `CryptoApi.getSecretStorageStatus` ([#5054](https://github.com/matrix-org/matrix-js-sdk/pull/5054)). Contributed by @richvdh.
|
||||
* Add parseCallNotificationContent ([#5015](https://github.com/matrix-org/matrix-js-sdk/pull/5015)). Contributed by @toger5.
|
||||
* MSC4140: support filters on delayed event lookup ([#5038](https://github.com/matrix-org/matrix-js-sdk/pull/5038)). Contributed by @AndrewFerr.
|
||||
|
||||
|
||||
Changes in [39.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.0.0) (2025-10-21)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* [MatrixRTC] Multi SFU support + m.rtc.member event type support ([#5022](https://github.com/matrix-org/matrix-js-sdk/pull/5022)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* [MatrixRTC] Multi SFU support + m.rtc.member event type support ([#5022](https://github.com/matrix-org/matrix-js-sdk/pull/5022)). Contributed by @toger5.
|
||||
* Implement Sticky Events MSC4354 ([#5028](https://github.com/matrix-org/matrix-js-sdk/pull/5028)). Contributed by @Half-Shot.
|
||||
* feat(client): allow disabling VoIP support ([#5021](https://github.com/matrix-org/matrix-js-sdk/pull/5021)). Contributed by @pkuzco.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Only use the first 3 viaServers specified ([#5034](https://github.com/matrix-org/matrix-js-sdk/pull/5034)). Contributed by @t3chguy.
|
||||
* Fetch the user's device info before processing a verification request ([#5030](https://github.com/matrix-org/matrix-js-sdk/pull/5030)). Contributed by @andybalaam.
|
||||
|
||||
|
||||
Changes in [38.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.4.0) (2025-10-07)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Add call intent to RTC call notifications ([#5010](https://github.com/matrix-org/matrix-js-sdk/pull/5010)). Contributed by @Half-Shot.
|
||||
* Implement experimental encrypted state events. ([#4994](https://github.com/matrix-org/matrix-js-sdk/pull/4994)). Contributed by @kaylendog.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Exclude cancelled requests from in-progress lists ([#5016](https://github.com/matrix-org/matrix-js-sdk/pull/5016)). Contributed by @andybalaam.
|
||||
|
||||
|
||||
Changes in [38.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.3.0) (2025-09-23)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Remove knocked room when membership changes to join ([#4977](https://github.com/matrix-org/matrix-js-sdk/pull/4977)). Contributed by @svajunas-budrys.
|
||||
|
||||
|
||||
Changes in [38.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.2.0) (2025-09-16)
|
||||
==================================================================================================
|
||||
Fix [CVE-2025-59160](https://www.cve.org/CVERecord?id=CVE-2025-59160) / [GHSA-mp7c-m3rh-r56v](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-mp7c-m3rh-r56v)
|
||||
|
||||
|
||||
|
||||
Changes in [38.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.1.0) (2025-09-09)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Remove custom `org.matrix.msc4075.rtc.notification.parent` relation type ([#4979](https://github.com/matrix-org/matrix-js-sdk/pull/4979)). Contributed by @toger5.
|
||||
* MatrixRTC: Add RTC decline event ([#4978](https://github.com/matrix-org/matrix-js-sdk/pull/4978)). Contributed by @toger5.
|
||||
* Make a MatrixRTCSession emit once the RTCNotification is sent ([#4976](https://github.com/matrix-org/matrix-js-sdk/pull/4976)). Contributed by @toger5.
|
||||
* Use hydra semantics for unknown room versions ([#4957](https://github.com/matrix-org/matrix-js-sdk/pull/4957)). Contributed by @dbkr.
|
||||
* Expose the StatusChanged event through the RTCSession ([#4974](https://github.com/matrix-org/matrix-js-sdk/pull/4974)). Contributed by @toger5.
|
||||
* Add `probablyLeft` event to the MatrixRTCSession ([#4962](https://github.com/matrix-org/matrix-js-sdk/pull/4962)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix `m.topic` format ([#4984](https://github.com/matrix-org/matrix-js-sdk/pull/4984)). Contributed by @tulir.
|
||||
* Fix stable-suffixed MSC4133 support ([#4983](https://github.com/matrix-org/matrix-js-sdk/pull/4983)). Contributed by @dbkr.
|
||||
* Fix thread edit aggregation race condition ([#4980](https://github.com/matrix-org/matrix-js-sdk/pull/4980)). Contributed by @basnijholt.
|
||||
|
||||
|
||||
Changes in [38.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.0.0) (2025-08-27)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Release tranche of breaking changes ([#4975](https://github.com/matrix-org/matrix-js-sdk/pull/4975)).
|
||||
* Remove support for FetchHttpApi `onlyData = false`
|
||||
* Remove deprecated `IJoinRoomOpts.syncRoom`
|
||||
* Remove deprecated methods which are unsupported in rust crypto
|
||||
* Remove deprecated getAuthIssuer method
|
||||
* Remove deprecated beginKeyVerification method
|
||||
* Remove deprecated isEncryptedDisabledForUnverifiedDevices getter
|
||||
* Remove deprecated UndecryptableToDeviceEvent MatrixClient emit
|
||||
* Remove deprecated defer utility method
|
||||
* Remove deprecated UIAResponse dummy type
|
||||
* Remove deprecated MatrixRTCSession MembershipConfig fields
|
||||
* Remove deprecated findVerificationRequestDMInProgress and storeSessionBackupPrivateKey methods in favour of overloads
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Allow multiple rtc sessions per room (with different sessionDescriptions) ([#4945](https://github.com/matrix-org/matrix-js-sdk/pull/4945)). Contributed by @toger5.
|
||||
* Add support for login\_hint in authorization url generation ([#4943](https://github.com/matrix-org/matrix-js-sdk/pull/4943)). Contributed by @odelcroi.
|
||||
* Only process MatrixRTC sessions associated with calls for `callMembershipsForRoom` ([#4960](https://github.com/matrix-org/matrix-js-sdk/pull/4960)). Contributed by @fkwp.
|
||||
|
||||
|
||||
Changes in [37.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.13.0) (2025-08-11)
|
||||
====================================================================================================
|
||||
This release supports new v12 Matrix rooms and consequently has a breaking change, removing powerLevelNorm from the RoomMember object as this can't be supported with infinite power levels. Apps should use the non-normalised `powerLevel` instead.
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* [Backport staging] Support for creator power level ([#4954](https://github.com/matrix-org/matrix-js-sdk/pull/4954)). Contributed by @RiotRobot.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* [Backport staging] Support v12 rooms in maySendEvent ([#4956](https://github.com/matrix-org/matrix-js-sdk/pull/4956)). Contributed by @RiotRobot.
|
||||
* [Backport staging] Support for creator power level ([#4954](https://github.com/matrix-org/matrix-js-sdk/pull/4954)). Contributed by @RiotRobot.
|
||||
* Experimental support for sharing encrypted history on invite ([#4920](https://github.com/matrix-org/matrix-js-sdk/pull/4920)). Contributed by @richvdh.
|
||||
* Use the logger associated with MatrixClient in rust sdk ([#4918](https://github.com/matrix-org/matrix-js-sdk/pull/4918)). Contributed by @richvdh.
|
||||
* Update to matrix-sdk-crypto-wasm 15.1.0, and add new `ShieldStateCode.MismatchedSender` ([#4916](https://github.com/matrix-org/matrix-js-sdk/pull/4916)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix unknown/broken state in the RTC Membership Manager causing unnecassary error logging. ([#4944](https://github.com/matrix-org/matrix-js-sdk/pull/4944)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [37.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.12.0) (2025-07-29)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate non-functional `IJoinRoomOpts.syncRoom` ([#4913](https://github.com/matrix-org/matrix-js-sdk/pull/4913)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Custom abort timeout logic for restarting delayed events that is compatible with the widget api ([#4927](https://github.com/matrix-org/matrix-js-sdk/pull/4927)). Contributed by @toger5.
|
||||
* Allow sending notification events when starting a call ([#4826](https://github.com/matrix-org/matrix-js-sdk/pull/4826)). Contributed by @robintown.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix deep import incompatibility (#4924) ([#4925](https://github.com/matrix-org/matrix-js-sdk/pull/4925)). Contributed by @toriningen.
|
||||
* Fix more incorrect logger use ([#4904](https://github.com/matrix-org/matrix-js-sdk/pull/4904)). Contributed by @richvdh.
|
||||
|
||||
|
||||
Changes in [37.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.11.0) (2025-07-15)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Update README to make Element sponsorship explicit ([#4901](https://github.com/matrix-org/matrix-js-sdk/pull/4901)). Contributed by @neilisfragile.
|
||||
* Use client logger in more places (crypto code) ([#4900](https://github.com/matrix-org/matrix-js-sdk/pull/4900)). Contributed by @richvdh.
|
||||
* Use client logger in `MatrixRTCSessionManager` ([#4898](https://github.com/matrix-org/matrix-js-sdk/pull/4898)). Contributed by @richvdh.
|
||||
* Use client logger in more places (core code) ([#4899](https://github.com/matrix-org/matrix-js-sdk/pull/4899)). Contributed by @richvdh.
|
||||
* crypto: Add new `ClientEvent.ReceivedToDeviceMessage` with proper `OlmEncryptionInfo` support ([#4891](https://github.com/matrix-org/matrix-js-sdk/pull/4891)). Contributed by @BillCarsonFr.
|
||||
|
||||
|
||||
Changes in [37.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.10.0) (2025-07-01)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ checks, so please check back after a few minutes.
|
||||
Your PR should include tests.
|
||||
|
||||
For new user facing features in `matrix-js-sdk`, you
|
||||
must include comprehensive unit tests written in Jest.
|
||||
must include comprehensive unit tests written in Vitest.
|
||||
The existing tests can be found under `spec/unit`
|
||||
|
||||
It's good practice to write tests alongside the code as it ensures the code is testable from
|
||||
|
||||
@@ -11,6 +11,22 @@
|
||||
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
|
||||
browser or in Node.js.
|
||||
|
||||
---
|
||||
|
||||
<picture>
|
||||
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
|
||||
<img src="contrib/element-logo-fallback.png" alt="Element logo">
|
||||
</picture>
|
||||
|
||||
<br>
|
||||
|
||||
Development and maintenance is proudly sponsored by [Element](https://element.io). Element uses the SDK in their flagship [web](https://github.com/element-hq/element-web) and [desktop](https://github.com/element-hq/element-desktop) clients.
|
||||
|
||||
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
|
||||
|
||||
---
|
||||
|
||||
#### Minimum Matrix server version: v1.1
|
||||
|
||||
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
|
||||
@@ -25,10 +41,10 @@ endpoints from before Matrix 1.1, for example.
|
||||
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
|
||||
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
|
||||
guide](https://pnpm.io/installation#using-corepack) if you do not have it already.
|
||||
|
||||
`yarn add matrix-js-sdk`
|
||||
`pnpm add matrix-js-sdk`
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
@@ -294,7 +310,7 @@ This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. Yo
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ yarn gendoc
|
||||
$ pnpm gendoc
|
||||
$ cd docs
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
@@ -437,7 +453,7 @@ want to use this SDK, skip this section._
|
||||
First, you need to pull in the right build tools:
|
||||
|
||||
```
|
||||
$ yarn install
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
## Building
|
||||
@@ -445,17 +461,17 @@ First, you need to pull in the right build tools:
|
||||
To build a browser version from scratch when developing:
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
$ pnpm build
|
||||
```
|
||||
|
||||
To run tests (Jest):
|
||||
To run tests:
|
||||
|
||||
```
|
||||
$ yarn test
|
||||
$ pnpm test
|
||||
```
|
||||
|
||||
To run linting:
|
||||
|
||||
```
|
||||
$ yarn lint
|
||||
$ pnpm lint
|
||||
```
|
||||
|
||||
+2
-10
@@ -7,21 +7,13 @@ module.exports = {
|
||||
targets: {
|
||||
esmodules: true,
|
||||
},
|
||||
// We want to output ES modules for the final build (mostly to ensure that
|
||||
// async imports work correctly). However, jest doesn't support ES modules very
|
||||
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
|
||||
// when testing.
|
||||
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"@babel/preset-typescript",
|
||||
{
|
||||
// When using the transpiled javascript in `lib`, Node.js requires `.js` extensions on any `import`
|
||||
// specifiers. However, Jest uses the TS source (via babel) and fails to resolve the `.js` names.
|
||||
// To resolve this,we use the `.ts` names in the source, and rewrite the `import` specifiers to use
|
||||
// `.js` during transpilation, *except* when we are targetting Jest.
|
||||
rewriteImportExtensions: process.env.NODE_ENV !== "test",
|
||||
rewriteImportExtensions: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
+4
-8
@@ -71,7 +71,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
11. If a variable is not receiving a value on declaration, its type must be defined.
|
||||
|
||||
```typescript
|
||||
let errorMessage: Optional<string>;
|
||||
let errorMessage: string;
|
||||
```
|
||||
|
||||
12. Objects can use shorthand declarations, including mixing of types.
|
||||
@@ -119,7 +119,6 @@ Unless otherwise specified, the following applies to all code:
|
||||
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
|
||||
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
|
||||
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
|
||||
|
||||
1. Note that an explicit type is optional if not expected to be used outside of the function call,
|
||||
unlike in this example:
|
||||
|
||||
@@ -151,9 +150,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
1. When using `any`, a comment explaining why must be present.
|
||||
27. `import` should be used instead of `require`, as `require` does not have types.
|
||||
28. Export only what can be reused.
|
||||
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
|
||||
of truly optional parameters.
|
||||
|
||||
29. Prefer a type like `X | null` instead of truly optional parameters.
|
||||
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
||||
takes an argument that is more often not required than required. An example where the
|
||||
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
||||
@@ -163,7 +160,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
```typescript
|
||||
function doThingWithRoom(
|
||||
thing: string,
|
||||
room: Optional<string>, // require the caller to specify
|
||||
room: string | null, // require the caller to specify
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
@@ -216,7 +213,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
## Tests
|
||||
|
||||
1. Tests must be written in TypeScript.
|
||||
2. Jest mocks are declared below imports, but above everything else.
|
||||
2. Mocks are declared below imports, but above everything else.
|
||||
3. Use the following convention template:
|
||||
|
||||
```typescript
|
||||
@@ -272,7 +269,6 @@ Unless otherwise specified, the following applies to all code:
|
||||
interfaces have a habit of becoming more complex over time.
|
||||
|
||||
3. Inside a function, there is no need to comment every line, but consider:
|
||||
|
||||
- before a particular multiline section of code within the function, give an overview of what it does,
|
||||
to make it easier for a reader to follow the flow through the function as a whole.
|
||||
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
|
||||
|
||||
# now run our checks
|
||||
cd "$tmpdir"
|
||||
yarn lint
|
||||
pnpm lint
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/* Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Config } from "jest";
|
||||
import { env } from "process";
|
||||
|
||||
const config: Config = {
|
||||
testEnvironment: "node",
|
||||
testMatch: ["<rootDir>/spec/**/*.spec.{js,ts}"],
|
||||
setupFilesAfterEnv: ["<rootDir>/spec/setupTests.ts"],
|
||||
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts}"],
|
||||
coverageReporters: ["text-summary", "lcov"],
|
||||
testResultsProcessor: "@casualbot/jest-sonar-reporter",
|
||||
|
||||
// Always print out a summary if there are any failing tests. Normally
|
||||
// a summary is only printed if there are more than 20 test *suites*.
|
||||
reporters: [["default", { summaryThreshold: 0 }]],
|
||||
};
|
||||
|
||||
// if we're running under GHA, enable the GHA reporter
|
||||
if (env["GITHUB_ACTIONS"] !== undefined) {
|
||||
const reporters: Config["reporters"] = [
|
||||
["github-actions", { silent: false }],
|
||||
// as above: always show a summary if there were any failing tests.
|
||||
["summary", { summaryThreshold: 0 }],
|
||||
];
|
||||
|
||||
// if we're running against the develop branch, also enable the slow test reporter
|
||||
if (env["GITHUB_REF"] == "refs/heads/develop") {
|
||||
reporters.push("<rootDir>/spec/slowReporter.cjs");
|
||||
}
|
||||
config.reporters = reporters;
|
||||
}
|
||||
|
||||
export default config;
|
||||
@@ -1,5 +1,8 @@
|
||||
import { KnipConfig } from "knip";
|
||||
|
||||
// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter'
|
||||
process.env.GITHUB_ACTIONS = "1";
|
||||
|
||||
export default {
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
@@ -12,10 +15,6 @@ export default {
|
||||
"src/utils.ts", // not really an entrypoint but we have deprecated `defer` there
|
||||
"scripts/**",
|
||||
"spec/**",
|
||||
// XXX: these look entirely unused
|
||||
"src/crypto/aes.ts",
|
||||
"src/crypto/crypto.ts",
|
||||
"src/crypto/recoverykey.ts",
|
||||
// XXX: these should be re-exported by one of the supported exports
|
||||
"src/matrixrtc/index.ts",
|
||||
"src/sliding-sync.ts",
|
||||
@@ -32,12 +31,6 @@ export default {
|
||||
"husky",
|
||||
// Used in script which only runs in environment with `@octokit/rest` installed
|
||||
"@octokit/rest",
|
||||
// Used by jest
|
||||
"jest-environment-jsdom",
|
||||
"babel-jest",
|
||||
"ts-node",
|
||||
// Used by `@babel/plugin-transform-runtime`
|
||||
"@babel/runtime",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
||||
|
||||
+48
-42
@@ -1,27 +1,26 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "37.10.0",
|
||||
"version": "41.3.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn clean && yarn build:compile && yarn build:types",
|
||||
"prepare": "pnpm build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel --delete-dir-on-start src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"build": "pnpm build:compile && pnpm build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile": "babel --delete-dir-on-start -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
|
||||
"lint": "pnpm lint:types && pnpm lint:js && pnpm lint:workflows",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
|
||||
"lint:knip": "knip",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"coverage": "pnpm test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -49,19 +48,18 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^15.0.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "11"
|
||||
"p-retry": "8",
|
||||
"sdp-transform": "^3.0.0",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
@@ -78,18 +76,19 @@
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@fetch-mock/vitest": "^0.2.18",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"@peculiar/webcrypto": "^1.4.5",
|
||||
"@stylistic/eslint-plugin": "^4.0.0",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/node": "22",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"@vitest/eslint-plugin": "^1.6.6",
|
||||
"@vitest/ui": "^4.0.17",
|
||||
"babel-plugin-search-and-replace": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "8.57.1",
|
||||
@@ -97,36 +96,43 @@
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jsdoc": "^50.0.0",
|
||||
"eslint-plugin-matrix-org": "2.1.0",
|
||||
"eslint-plugin-jsdoc": "^62.0.0",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-tsdoc": "^0.5.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "11.1.5",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"fetch-mock": "^12.6.0",
|
||||
"happy-dom": "^20.1.0",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"knip": "^5.0.0",
|
||||
"knip": "^6.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "3.5.3",
|
||||
"rimraf": "^6.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"prettier": "3.8.3",
|
||||
"typedoc": "^0.28.1",
|
||||
"typedoc-plugin-coverage": "^4.0.0",
|
||||
"typedoc-plugin-mdn-links": "^5.0.0",
|
||||
"typedoc-plugin-missing-exports": "^4.0.0",
|
||||
"typescript": "^5.4.2"
|
||||
"typescript": "^6.0.0",
|
||||
"vitest": "^4.0.17",
|
||||
"vitest-sonar-reporter": "^3.0.0"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
}
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"eslint": "8"
|
||||
}
|
||||
},
|
||||
"allowedDeprecatedVersions": {
|
||||
"eslint": "8"
|
||||
},
|
||||
"overrides": {
|
||||
"expect": "30.3.0",
|
||||
"flatted@<=3.4.1": "^3.4.2",
|
||||
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
|
||||
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
|
||||
"vite": "8.0.8"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||
}
|
||||
|
||||
Generated
+8353
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
nodeLinker: hoisted
|
||||
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=spec/**/*
|
||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
||||
sonar.testExecutionReportPaths=coverage/sonar-report.xml
|
||||
|
||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||
|
||||
+2
-5
@@ -17,10 +17,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "./olm-loader";
|
||||
/* eslint-disable @vitest/no-standalone-expect */
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
@@ -42,7 +39,7 @@ import { type ISyncResponder } from "./test-utils/SyncResponder";
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock to intercept the HTTP requests, and
|
||||
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
|
||||
*/
|
||||
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
|
||||
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import debug from "debug";
|
||||
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
import { type AuthDict, createClient, type MatrixClient } from "../../../src";
|
||||
import { type AuthDict, createClient, DebugLogger, type MatrixClient } from "../../../src";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
|
||||
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||
import { type CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
|
||||
@@ -73,7 +74,7 @@ describe("cross-signing", () => {
|
||||
function createCryptoCallbacks(): CryptoCallbacks {
|
||||
return {
|
||||
getSecretStorageKey: (keys, name) => {
|
||||
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
|
||||
return Promise.resolve<[string, Uint8Array<ArrayBuffer>]>(["key_id", encryptionKey]);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -82,7 +83,6 @@ describe("cross-signing", () => {
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
@@ -91,6 +91,7 @@ describe("cross-signing", () => {
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
cryptoCallbacks: createCryptoCallbacks(),
|
||||
logger: new DebugLogger(debug(`matrix-js-sdk:cross-signing`)),
|
||||
});
|
||||
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
@@ -111,8 +112,7 @@ describe("cross-signing", () => {
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -135,28 +135,26 @@ describe("cross-signing", () => {
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// check the cross-signing keys upload
|
||||
expect(fetchMock.called("upload-keys")).toBeTruthy();
|
||||
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
|
||||
// check that the cross-signing keys have been uploaded
|
||||
expect(fetchMock.callHistory.called("upload-cross-signing-keys")).toBeTruthy();
|
||||
const keysOpts = fetchMock.callHistory.lastCall("upload-cross-signing-keys")!.options;
|
||||
const keysBody = JSON.parse(keysOpts!.body as string);
|
||||
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
||||
// there should be a key of each type
|
||||
// master key is signed by the device
|
||||
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
|
||||
expect(keysBody).toHaveProperty(["master_key", "signatures", TEST_USER_ID, `ed25519:${TEST_DEVICE_ID}`]);
|
||||
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
|
||||
// ssk and usk are signed by the master key
|
||||
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(["self_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
|
||||
expect(keysBody).toHaveProperty(["user_signing_key", "signatures", TEST_USER_ID, masterKeyId]);
|
||||
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
||||
|
||||
// check the publish call
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// there should be a signature for our device, by our self-signing key.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
|
||||
);
|
||||
expect(body).toHaveProperty([TEST_USER_ID, TEST_DEVICE_ID, "signatures", TEST_USER_ID, sskId]);
|
||||
});
|
||||
|
||||
it("get cross signing keys from secret storage and import them", async () => {
|
||||
@@ -223,9 +221,6 @@ describe("cross-signing", () => {
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
|
||||
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
|
||||
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
|
||||
@@ -238,13 +233,17 @@ describe("cross-signing", () => {
|
||||
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
|
||||
|
||||
// Expect the signature to be uploaded
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
expect(fetchMock.callHistory.called("upload-sigs")).toBeTruthy();
|
||||
const sigsOpts = fetchMock.callHistory.lastCall("upload-sigs")!.options;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// the device should have a signature with the public self cross signing keys.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
|
||||
);
|
||||
expect(body).toHaveProperty([
|
||||
TEST_USER_ID,
|
||||
TEST_DEVICE_ID,
|
||||
"signatures",
|
||||
TEST_USER_ID,
|
||||
`ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("can bootstrapCrossSigning twice", async () => {
|
||||
@@ -256,8 +255,7 @@ describe("cross-signing", () => {
|
||||
// a second call should do nothing except GET requests
|
||||
fetchMock.mockClear();
|
||||
await bootstrapCrossSigning(authDict);
|
||||
const calls = fetchMock.calls((url, opts) => opts.method != "GET");
|
||||
expect(calls.length).toEqual(0);
|
||||
expect(fetchMock).toHaveFetchedTimes(0, "unmatched");
|
||||
});
|
||||
|
||||
it("will upload existing cross-signing keys to an established secret storage", async () => {
|
||||
@@ -268,7 +266,6 @@ describe("cross-signing", () => {
|
||||
// To arrange that, we call `bootstrapCrossSigning` on our main device, and then (pretend to) set up 4S from
|
||||
// a *different* device. Then, when we call `bootstrapCrossSigning` again, it should do the honours.
|
||||
|
||||
mockSetupCrossSigningRequests();
|
||||
const accountDataAccumulator = new AccountDataAccumulator(syncResponder);
|
||||
accountDataAccumulator.interceptGetAccountData();
|
||||
|
||||
@@ -283,7 +280,7 @@ describe("cross-signing", () => {
|
||||
});
|
||||
|
||||
// Prepare for the cross-signing keys
|
||||
const p = accountDataAccumulator.interceptSetAccountData(":type(m.cross_signing..*)");
|
||||
const p = accountDataAccumulator.waitForAccountData("m.cross_signing.master");
|
||||
|
||||
await bootstrapCrossSigning(authDict);
|
||||
await p;
|
||||
@@ -404,7 +401,7 @@ describe("cross-signing", () => {
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeFalsy();
|
||||
});
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe("getCrossSigningKeyId", () => {
|
||||
@@ -416,19 +413,13 @@ describe("cross-signing", () => {
|
||||
*/
|
||||
function awaitCrossSigningKeysUpload() {
|
||||
return new Promise<any>((resolve) => {
|
||||
fetchMock.post(
|
||||
{
|
||||
url: new RegExp("/_matrix/client/v3/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
(url, options) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
fetchMock.modifyRoute("upload-cross-signing-keys", {
|
||||
response: (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return {};
|
||||
},
|
||||
// Override the routes define in `mockSetupCrossSigningRequests`
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -459,9 +450,6 @@ describe("cross-signing", () => {
|
||||
|
||||
describe("crossSignDevice", () => {
|
||||
beforeEach(async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
// make sure that there is another device which we can sign
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
@@ -475,10 +463,6 @@ describe("cross-signing", () => {
|
||||
expect(devices.get(aliceClient.getSafeUserId())!.has(testData.TEST_DEVICE_ID)).toBeTruthy();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("fails for an unknown device", async () => {
|
||||
await expect(aliceClient.getCrypto()!.crossSignDevice("unknown")).rejects.toThrow("Unknown device");
|
||||
});
|
||||
@@ -491,9 +475,9 @@ describe("cross-signing", () => {
|
||||
await aliceClient.getCrypto()!.crossSignDevice(testData.TEST_DEVICE_ID);
|
||||
|
||||
// check that a sig for the device was uploaded
|
||||
const calls = fetchMock.calls("upload-sigs");
|
||||
const calls = fetchMock.callHistory.calls("upload-sigs");
|
||||
expect(calls.length).toEqual(1);
|
||||
const body = JSON.parse(calls[0][1]!.body as string);
|
||||
const body = JSON.parse(calls[0].options!.body as string);
|
||||
const deviceSig = body[aliceClient.getSafeUserId()][testData.TEST_DEVICE_ID];
|
||||
expect(deviceSig).toHaveProperty("signatures");
|
||||
});
|
||||
|
||||
+225
-256
@@ -16,12 +16,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import Olm from "@matrix-org/olm";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
|
||||
import type FetchMock from "fetch-mock";
|
||||
import type Olm from "@matrix-org/olm";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import {
|
||||
emitPromise,
|
||||
@@ -86,9 +86,13 @@ import {
|
||||
encryptGroupSessionKey,
|
||||
encryptMegolmEvent,
|
||||
encryptMegolmEventRawPlainText,
|
||||
encryptOlmEvent,
|
||||
establishOlmSession,
|
||||
getTestOlmAccountKeys,
|
||||
} from "./olm-utils";
|
||||
expectSendRoomKey,
|
||||
expectSendMegolmMessageEvent,
|
||||
expectEncryptedSendMessageEvent,
|
||||
} from "./olm-utils.ts";
|
||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
@@ -101,120 +105,10 @@ afterEach(() => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
/**
|
||||
* Expect that the client shares keys with the given recipient
|
||||
*
|
||||
* Waits for an HTTP request to send the encrypted m.room_key to-device message; decrypts it and uses it
|
||||
* to establish an Olm InboundGroupSession.
|
||||
*
|
||||
* @param recipientUserID - the user id of the expected recipient
|
||||
*
|
||||
* @param recipientOlmAccount - Olm.Account for the recipient
|
||||
*
|
||||
* @param recipientOlmSession - an Olm.Session for the recipient, which must already have exchanged pre-key
|
||||
* messages with the sender. Alternatively, null, in which case we will expect a pre-key message.
|
||||
*
|
||||
* @returns the established inbound group session
|
||||
*/
|
||||
async function expectSendRoomKey(
|
||||
recipientUserID: string,
|
||||
recipientOlmAccount: Olm.Account,
|
||||
recipientOlmSession: Olm.Session | null = null,
|
||||
): Promise<Olm.InboundGroupSession> {
|
||||
const Olm = globalThis.Olm;
|
||||
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
|
||||
|
||||
function onSendRoomKey(content: any): Olm.InboundGroupSession {
|
||||
const m = content.messages[recipientUserID].DEVICE_ID;
|
||||
const ct = m.ciphertext[testRecipientKey];
|
||||
|
||||
if (!recipientOlmSession) {
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
recipientOlmSession = new Olm.Session();
|
||||
recipientOlmSession.create_inbound(recipientOlmAccount, ct.body);
|
||||
} else {
|
||||
expect(ct.type).toEqual(1); // regular message
|
||||
}
|
||||
|
||||
const decrypted = JSON.parse(recipientOlmSession.decrypt(ct.type, ct.body));
|
||||
expect(decrypted.type).toEqual("m.room_key");
|
||||
const inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return inboundGroupSession;
|
||||
}
|
||||
return await new Promise<Olm.InboundGroupSession>((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp("/sendToDevice/m.room.encrypted/"),
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
const content = JSON.parse(opts.body as string);
|
||||
resolve(onSendRoomKey(content));
|
||||
return {};
|
||||
},
|
||||
{
|
||||
// append to the list of intercepts on this path (since we have some tests that call
|
||||
// this function multiple times)
|
||||
overwriteRoutes: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the event received on rooms/{roomId}/send/m.room.encrypted endpoint.
|
||||
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
|
||||
* @returns the content of the encrypted event
|
||||
*/
|
||||
function expectEncryptedSendMessage() {
|
||||
return new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp("/send/m.room.encrypted/"),
|
||||
(url, request) => {
|
||||
const content = JSON.parse(request.body as string);
|
||||
resolve(content);
|
||||
return { event_id: "$event_id" };
|
||||
},
|
||||
// append to the list of intercepts on this path (since we have some tests that call
|
||||
// this function multiple times)
|
||||
{ overwriteRoutes: false },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client sends an encrypted event
|
||||
*
|
||||
* Waits for an HTTP request to send an encrypted message in the test room.
|
||||
*
|
||||
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
|
||||
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
|
||||
*
|
||||
* @returns The content of the successfully-decrypted event
|
||||
*/
|
||||
async function expectSendMegolmMessage(
|
||||
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
|
||||
): Promise<Partial<IEvent>> {
|
||||
const encryptedMessageContent = await expectEncryptedSendMessage();
|
||||
|
||||
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
|
||||
const inboundGroupSession = await inboundGroupSessionPromise;
|
||||
|
||||
const r: any = inboundGroupSession.decrypt(encryptedMessageContent!.ciphertext);
|
||||
logger.log("Decrypted received megolm message", r);
|
||||
return JSON.parse(r.plaintext);
|
||||
}
|
||||
|
||||
describe("crypto", () => {
|
||||
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;
|
||||
}
|
||||
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
let testSenderKey = "";
|
||||
|
||||
@@ -261,13 +155,8 @@ describe("crypto", () => {
|
||||
return response;
|
||||
}
|
||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||
fetchMock.postOnce(
|
||||
new RegExp(rootRegexp + "(r0|v3)/keys/query"),
|
||||
(url: string, opts: RequestInit) => onQueryRequest(JSON.parse(opts.body as string)),
|
||||
{
|
||||
// append to the list of intercepts on this path
|
||||
overwriteRoutes: false,
|
||||
},
|
||||
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/query"), (callLog) =>
|
||||
onQueryRequest(JSON.parse(callLog.options.body as string)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -277,7 +166,7 @@ describe("crypto", () => {
|
||||
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
|
||||
* (or a function that returns one).
|
||||
*/
|
||||
function expectAliceKeyClaim(response: FetchMock.MockResponse | FetchMock.MockResponseFunction) {
|
||||
function expectAliceKeyClaim(response: RouteResponse) {
|
||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
|
||||
}
|
||||
@@ -330,15 +219,20 @@ describe("crypto", () => {
|
||||
*/
|
||||
function createCryptoCallbacks(): CryptoCallbacks {
|
||||
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
|
||||
let cachedKey: { keyId: string; key: Uint8Array };
|
||||
const cacheSecretStorageKey = (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => {
|
||||
let cachedKey: { keyId: string; key: Uint8Array<ArrayBuffer> };
|
||||
const cacheSecretStorageKey = (
|
||||
keyId: string,
|
||||
keyInfo: SecretStorageKeyDescription,
|
||||
key: Uint8Array<ArrayBuffer>,
|
||||
) => {
|
||||
cachedKey = {
|
||||
keyId,
|
||||
key,
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);
|
||||
const getSecretStorageKey = () =>
|
||||
Promise.resolve<[string, Uint8Array<ArrayBuffer>]>([cachedKey.keyId, cachedKey.key]);
|
||||
|
||||
return {
|
||||
cacheSecretStorageKey,
|
||||
@@ -350,7 +244,6 @@ describe("crypto", () => {
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
@@ -372,18 +265,20 @@ describe("crypto", () => {
|
||||
testOlmAccount = await createOlmAccount();
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
|
||||
vi.useRealTimers();
|
||||
},
|
||||
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
|
||||
10000,
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
aliceClient.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
if (vi.isFakeTimers()) {
|
||||
await vi.runAllTimersAsync();
|
||||
}
|
||||
});
|
||||
|
||||
it("MatrixClient.getCrypto returns a CryptoApi", () => {
|
||||
@@ -450,7 +345,7 @@ describe("crypto", () => {
|
||||
|
||||
describe("Unable to decrypt error codes", function () {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it("Decryption fails with UISI error", async () => {
|
||||
@@ -826,7 +721,7 @@ describe("crypto", () => {
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
await awaitDecryptionError;
|
||||
await expect(awaitDecryptionError).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -977,6 +872,7 @@ describe("crypto", () => {
|
||||
await expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Alice sends a megolm message", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
@@ -1000,10 +896,11 @@ describe("crypto", () => {
|
||||
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
|
||||
await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendMegolmMessage(inboundGroupSessionPromise),
|
||||
expectSendMegolmMessageEvent(inboundGroupSessionPromise),
|
||||
]);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("We should start a new megolm session after forceDiscardSession", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
@@ -1027,7 +924,7 @@ describe("crypto", () => {
|
||||
// Send the first message, and check we can decrypt it.
|
||||
await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendMegolmMessage(inboundGroupSessionPromise),
|
||||
expectSendMegolmMessageEvent(inboundGroupSessionPromise),
|
||||
]);
|
||||
|
||||
// Finally the interesting part: discard the session.
|
||||
@@ -1035,7 +932,7 @@ describe("crypto", () => {
|
||||
|
||||
// Now when we send the next message, we should get a *new* megolm session.
|
||||
const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
const p2 = expectSendMegolmMessage(inboundGroupSessionPromise2);
|
||||
const p2 = expectSendMegolmMessageEvent(inboundGroupSessionPromise2);
|
||||
await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]);
|
||||
});
|
||||
|
||||
@@ -1046,7 +943,7 @@ describe("crypto", () => {
|
||||
*/
|
||||
async function sendEncryptedMessage(): Promise<IContent> {
|
||||
const [encryptedMessage] = await Promise.all([
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
]);
|
||||
return encryptedMessage;
|
||||
@@ -1104,9 +1001,7 @@ describe("crypto", () => {
|
||||
await startClientAndAwaitFirstSync();
|
||||
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount);
|
||||
|
||||
// We need to fake the timers to advance the time, but the wasm bindings of matrix-sdk-crypto rely on a
|
||||
// working `queueMicrotask`
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
const syncResponse = getSyncResponse(["@bob:xyz"]);
|
||||
|
||||
@@ -1138,7 +1033,7 @@ describe("crypto", () => {
|
||||
expect(sessionId).toBeDefined();
|
||||
|
||||
// Advance the time by 1h
|
||||
jest.advanceTimersByTime(oneHourInMs);
|
||||
vi.advanceTimersByTime(oneHourInMs);
|
||||
|
||||
// Send a second message to bob and get the encrypted message
|
||||
const [secondEncryptedMessage] = await Promise.all([
|
||||
@@ -1168,7 +1063,7 @@ describe("crypto", () => {
|
||||
let [, , encryptedMessage] = await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
]);
|
||||
|
||||
// Check that the session id exists
|
||||
@@ -1196,7 +1091,7 @@ describe("crypto", () => {
|
||||
[, , encryptedMessage] = await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession),
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
]);
|
||||
|
||||
// Check that the new session id exists
|
||||
@@ -1259,7 +1154,7 @@ describe("crypto", () => {
|
||||
// it probably won't be decrypted yet, because it takes a while to process the olm keys
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID);
|
||||
expect(decryptedEvent.getContent()).toEqual({});
|
||||
expect(decryptedEvent.getContent<IContent>()).toEqual({});
|
||||
expect(decryptedEvent.getClearContent()).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -1283,20 +1178,12 @@ describe("crypto", () => {
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes.
|
||||
const sendRoomMessageResolvers = Promise.withResolvers<FetchMock.MockResponse>();
|
||||
const sendRoomMessageResolvers = Promise.withResolvers<RouteResponse>();
|
||||
const reqProm = new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp("/send/m.room.encrypted/"),
|
||||
async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
return await sendRoomMessageResolvers.promise;
|
||||
},
|
||||
{
|
||||
// append to the list of intercepts on this path (since we have some tests that call
|
||||
// this function multiple times)
|
||||
overwriteRoutes: false,
|
||||
},
|
||||
);
|
||||
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), async (callLog): Promise<RouteResponse> => {
|
||||
resolve(JSON.parse(callLog.options.body as string));
|
||||
return await sendRoomMessageResolvers.promise;
|
||||
});
|
||||
});
|
||||
|
||||
// Now we start to send the message
|
||||
@@ -1379,6 +1266,7 @@ describe("crypto", () => {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Sending an event initiates a member list sync", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
@@ -1394,7 +1282,7 @@ describe("crypto", () => {
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
|
||||
|
||||
// and finally the megolm message
|
||||
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise);
|
||||
const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
|
||||
|
||||
// kick it off
|
||||
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
|
||||
@@ -1402,6 +1290,7 @@ describe("crypto", () => {
|
||||
await Promise.all([sendPromise, megolmMessagePromise, memberListPromise]);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("loading the membership list inhibits a later load", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
@@ -1417,7 +1306,7 @@ describe("crypto", () => {
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount, p2pSession);
|
||||
|
||||
// and finally the megolm message
|
||||
const megolmMessagePromise = expectSendMegolmMessage(inboundGroupSessionPromise);
|
||||
const megolmMessagePromise = expectSendMegolmMessageEvent(inboundGroupSessionPromise);
|
||||
|
||||
// kick it off
|
||||
const sendPromise = aliceClient.sendTextMessage(ROOM_ID, "test");
|
||||
@@ -1500,8 +1389,10 @@ describe("crypto", () => {
|
||||
|
||||
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
|
||||
|
||||
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors.
|
||||
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified");
|
||||
// `decryptionFailureReason` should be `MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE` for `m.unverified`
|
||||
expect(
|
||||
ev.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
).toEqual(withheldCode === "m.unverified");
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1509,33 +1400,26 @@ describe("crypto", () => {
|
||||
|
||||
describe("key upload request", () => {
|
||||
beforeEach(() => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
|
||||
return new Promise((resolve) => {
|
||||
const listener = (url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const keysCount = Object.keys(content?.one_time_keys || {}).length;
|
||||
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
|
||||
if (keysCount) resolve({ keysCount, fallbackKeysCount });
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
// The matrix client does `/upload` requests until 50 keys are uploaded
|
||||
// We return here 60 to avoid the `/upload` request loop
|
||||
signed_curve25519: keysCount ? 60 : keysCount,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
for (const path of ["/_matrix/client/v3/keys/upload", "/_matrix/client/v3/keys/upload"]) {
|
||||
fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener, {
|
||||
// These routes are already defined in the E2EKeyReceiver
|
||||
// We want to overwrite the behaviour of the E2EKeyReceiver
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
}
|
||||
fetchMock.modifyRoute("keys-upload", {
|
||||
response: (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
const keysCount = Object.keys(content?.one_time_keys || {}).length;
|
||||
const fallbackKeysCount = Object.keys(content?.fallback_keys || {}).length;
|
||||
if (keysCount) resolve({ keysCount, fallbackKeysCount });
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
// The matrix client does `/upload` requests until 50 keys are uploaded
|
||||
// We return here 60 to avoid the `/upload` request loop
|
||||
signed_curve25519: keysCount ? 60 : keysCount,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1548,7 +1432,7 @@ describe("crypto", () => {
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Verify that `/upload` is called on Alice's homesever
|
||||
// Verify that `/upload` is called on Alice's homeserver
|
||||
const { keysCount, fallbackKeysCount } = await uploadPromise;
|
||||
expect(keysCount).toBeGreaterThan(0);
|
||||
expect(fallbackKeysCount).toBe(0);
|
||||
@@ -1562,7 +1446,7 @@ describe("crypto", () => {
|
||||
|
||||
// Advance local date to 2 minutes
|
||||
// The old crypto only runs the upload every 60 seconds
|
||||
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
vi.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
@@ -1653,18 +1537,16 @@ describe("crypto", () => {
|
||||
|
||||
function awaitKeyQueryRequest(): Promise<Record<string, []>> {
|
||||
return new Promise((resolve) => {
|
||||
const listener = (url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
// Resolve with request payload
|
||||
resolve(content.device_keys);
|
||||
|
||||
// Return response of `/keys/query`
|
||||
return queryResponseBody;
|
||||
};
|
||||
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/query", aliceClient.getHomeserverUrl()).toString(),
|
||||
listener,
|
||||
(callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
// Resolve with request payload
|
||||
resolve(content.device_keys);
|
||||
|
||||
// Return response of `/keys/query`
|
||||
return queryResponseBody;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1703,8 +1585,7 @@ describe("crypto", () => {
|
||||
});
|
||||
|
||||
it("Get devices from tracked users", async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -1716,12 +1597,12 @@ describe("crypto", () => {
|
||||
|
||||
// Advance local date to 2 minutes
|
||||
// The old crypto only runs the upload every 60 seconds
|
||||
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
vi.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Old crypto: for alice: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
// Old crypto: for alice: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
|
||||
await flushPromises();
|
||||
|
||||
@@ -1729,7 +1610,7 @@ describe("crypto", () => {
|
||||
await queryPromise;
|
||||
|
||||
// Old crypto: for `user`: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
// Old crypto: for `user`: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
|
||||
// It will add `@testing_florian1:matrix.org` devices to the DeviceList
|
||||
await flushPromises();
|
||||
@@ -1753,7 +1634,7 @@ describe("crypto", () => {
|
||||
* Create a fake secret storage key
|
||||
* Async because `bootstrapSecretStorage` expect an async method
|
||||
*/
|
||||
const createSecretStorageKey = jest.fn().mockResolvedValue({
|
||||
const createSecretStorageKey = vi.fn().mockResolvedValue({
|
||||
keyInfo: {}, // Returning undefined here used to cause a crash
|
||||
privateKey: Uint8Array.of(32, 33),
|
||||
});
|
||||
@@ -1771,7 +1652,7 @@ describe("crypto", () => {
|
||||
* https://spec.matrix.org/v1.6/client-server-api/#put_matrixclientv3useruseridaccount_datatype
|
||||
*/
|
||||
async function awaitCrossSigningKeyUpload(key: string): Promise<Record<string, {}>> {
|
||||
const content = await accountDataAccumulator.interceptSetAccountData(`m.cross_signing.${key}`);
|
||||
const content = await accountDataAccumulator.waitForAccountData(`m.cross_signing.${key}`);
|
||||
return content.encrypted;
|
||||
}
|
||||
|
||||
@@ -1783,10 +1664,7 @@ describe("crypto", () => {
|
||||
async function awaitSecretStorageKeyStoredInAccountData(): Promise<string> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const content = await accountDataAccumulator.interceptSetAccountData(":type(m.secret_storage.*)", {
|
||||
repeat: 1,
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
const content = await accountDataAccumulator.waitForAccountData("m.secret_storage.*");
|
||||
if (content.key) {
|
||||
return content.key;
|
||||
}
|
||||
@@ -1794,10 +1672,7 @@ describe("crypto", () => {
|
||||
}
|
||||
|
||||
async function awaitMegolmBackupKeyUpload(): Promise<Record<string, {}>> {
|
||||
const content = await accountDataAccumulator.interceptSetAccountData("m.megolm_backup.v1", {
|
||||
repeat: 1,
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
const content = await accountDataAccumulator.waitForAccountData("m.megolm_backup.v1");
|
||||
return content.encrypted;
|
||||
}
|
||||
|
||||
@@ -1818,7 +1693,6 @@ describe("crypto", () => {
|
||||
* @param backupVersion - The version of the created backup
|
||||
*/
|
||||
async function bootstrapSecurity(backupVersion: string): Promise<void> {
|
||||
mockSetupCrossSigningRequests();
|
||||
mockSetupMegolmBackupRequests(backupVersion);
|
||||
|
||||
// promise which will resolve when a `KeyBackupStatus` event is emitted with `enabled: true`
|
||||
@@ -1897,6 +1771,7 @@ describe("crypto", () => {
|
||||
|
||||
it("Should create a 4S key", async () => {
|
||||
accountDataAccumulator.interceptGetAccountData();
|
||||
accountDataAccumulator.interceptSetAccountData();
|
||||
|
||||
const awaitAccountData = awaitAccountDataUpdate("m.secret_storage.default_key");
|
||||
|
||||
@@ -2048,8 +1923,7 @@ describe("crypto", () => {
|
||||
|
||||
describe("Manage Key Backup", () => {
|
||||
beforeEach(async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it("Should be able to restore from 4S after bootstrap", async () => {
|
||||
@@ -2066,29 +1940,23 @@ describe("crypto", () => {
|
||||
const newKey = testData.MEGOLM_SESSION_DATA;
|
||||
|
||||
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
(url, request) => {
|
||||
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
|
||||
resolve(uploadPayload);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 1,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.put("path:/_matrix/client/v3/room_keys/keys", (callLog) => {
|
||||
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
resolve(uploadPayload);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 1,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
await aliceClient.getCrypto()!.importRoomKeys([newKey]);
|
||||
|
||||
// The backup loop waits a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
const keyBackupData = await awaitKeyUploaded;
|
||||
|
||||
@@ -2116,40 +1984,33 @@ describe("crypto", () => {
|
||||
fetchMock.delete(
|
||||
"express:/_matrix/client/v3/room_keys/version/:version",
|
||||
(url: string, options: RequestInit) => {
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
fetchMock.modifyRoute("room-keys-version", {
|
||||
response: {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
resolve();
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
|
||||
const newVersion = "2";
|
||||
fetchMock.post(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
(url, request) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
|
||||
fetchMock.modifyRoute("post-room-keys-version", {
|
||||
response: (callLog) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
backupData.version = newVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
|
||||
// update get call with new version
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.modifyRoute("room-keys-version", { response: backupData });
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
|
||||
const newBackupStatusUpdate = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
@@ -2204,6 +2065,7 @@ describe("crypto", () => {
|
||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
expect(verificationStatus.known).toBe(false); // We haven't actually stashed a copy of Alice's identity
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
@@ -2211,15 +2073,15 @@ describe("crypto", () => {
|
||||
});
|
||||
|
||||
it("Cross signing keys are available for a tracked user", async () => {
|
||||
// Process Alice keys, old crypto has a sleep(5ms) during the process
|
||||
await jest.advanceTimersByTimeAsync(5);
|
||||
// Process Alice keys
|
||||
await flushPromises();
|
||||
|
||||
// Alice is the local user and should be tracked !
|
||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
|
||||
expect(hasCrossSigningKeysForUser).toBe(true);
|
||||
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
|
||||
expect(verificationStatus.known).toBe(true);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
@@ -2230,7 +2092,8 @@ describe("crypto", () => {
|
||||
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
|
||||
expect(hasCrossSigningKeysForUser).toBe(false);
|
||||
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus("@unknown:xyz");
|
||||
expect(verificationStatus.known).toBe(false);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
@@ -2260,6 +2123,7 @@ describe("crypto", () => {
|
||||
|
||||
{
|
||||
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
|
||||
expect(verificationStatus.known).toBe(true);
|
||||
expect(verificationStatus.isVerified()).toBe(false);
|
||||
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
|
||||
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
|
||||
@@ -2300,6 +2164,7 @@ describe("crypto", () => {
|
||||
client2?.stopClient();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
test("Sending a message in a room where the server is hiding the state event does not send a plaintext event", async () => {
|
||||
// Alice is in an encrypted room
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2" });
|
||||
@@ -2307,7 +2172,7 @@ describe("crypto", () => {
|
||||
await syncPromise(client1);
|
||||
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
await Promise.all([client1.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessage()]);
|
||||
await Promise.all([client1.sendTextMessage(ROOM_ID, "test"), expectEncryptedSendMessageEvent()]);
|
||||
|
||||
// We now replace the client, and allow the new one to resync, *without* the encryption event.
|
||||
client2 = await replaceClient(client1);
|
||||
@@ -2328,7 +2193,7 @@ describe("crypto", () => {
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
const [, msg1Content] = await Promise.all([
|
||||
client1.sendTextMessage(ROOM_ID, "test1"),
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
]);
|
||||
|
||||
// Replace the state with one which bumps the rotation period. This should be ignored, though it's not
|
||||
@@ -2347,16 +2212,17 @@ describe("crypto", () => {
|
||||
// use a different one.
|
||||
const [, msg2Content] = await Promise.all([
|
||||
client1.sendTextMessage(ROOM_ID, "test2"),
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
]);
|
||||
expect(msg2Content.session_id).toEqual(msg1Content.session_id);
|
||||
const [, msg3Content] = await Promise.all([
|
||||
client1.sendTextMessage(ROOM_ID, "test3"),
|
||||
expectEncryptedSendMessage(),
|
||||
expectEncryptedSendMessageEvent(),
|
||||
]);
|
||||
expect(msg3Content.session_id).not.toEqual(msg1Content.session_id);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
test("Changes to the rotation period should be ignored after a client restart", async () => {
|
||||
// Alice is in an encrypted room, where the rotation period is set to 2 messages
|
||||
const encryptionState = mkEncryptionEvent({ algorithm: "m.megolm.v1.aes-sha2", rotation_period_msgs: 2 });
|
||||
@@ -2364,7 +2230,7 @@ describe("crypto", () => {
|
||||
await syncPromise(client1);
|
||||
|
||||
// Send a message, and expect to get an `m.room.encrypted` event.
|
||||
await Promise.all([client1.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessage()]);
|
||||
await Promise.all([client1.sendTextMessage(ROOM_ID, "test1"), expectEncryptedSendMessageEvent()]);
|
||||
|
||||
// We now replace the client, and allow the new one to resync with a *different* encryption event.
|
||||
client2 = await replaceClient(client1);
|
||||
@@ -2450,4 +2316,107 @@ describe("crypto", () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
describe("secret pushing", () => {
|
||||
it("should push a new backup key when a new backup key is set", async () => {
|
||||
// setup: alice has another device, DEVICE_ID, which is verified
|
||||
const crypto = aliceClient.getCrypto()!;
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
|
||||
await startClientAndAwaitFirstSync();
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
|
||||
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
|
||||
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
|
||||
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
|
||||
|
||||
// when we set a new backup key
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 200,
|
||||
body: { version: "1" },
|
||||
});
|
||||
const secretPushPromise = new Promise<any>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
await crypto.resetKeyBackup();
|
||||
|
||||
// we expect the other device to get a secret push
|
||||
const content = await secretPushPromise;
|
||||
const curve25519key = JSON.parse(testOlmAccount.identity_keys()).curve25519;
|
||||
const ciphertext = content.messages["@alice:localhost"].DEVICE_ID.ciphertext[curve25519key];
|
||||
const olmSession = new Olm.Session();
|
||||
olmSession.create_inbound(testOlmAccount, ciphertext.body);
|
||||
const decrypted = JSON.parse(olmSession.decrypt(0, ciphertext.body));
|
||||
expect(decrypted.type).toBe("io.element.msc4385.secret.push");
|
||||
expect(decrypted.content.name).toBe("m.megolm_backup.v1");
|
||||
});
|
||||
|
||||
it("should receive pushed backup key", async () => {
|
||||
// setup: alice has another device, DEVICE_ID, which is verified,
|
||||
// and has a key backup set up and signed by DEVICE_ID
|
||||
const crypto = aliceClient.getCrypto()!;
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
await startClientAndAwaitFirstSync();
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
|
||||
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
|
||||
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
|
||||
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
|
||||
|
||||
// after we push the backup key to alice...
|
||||
|
||||
const senderIdentityKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
const aliceDeviceKeys = await crypto.getOwnDeviceKeys();
|
||||
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
|
||||
const secretPush = encryptOlmEvent({
|
||||
sender: "@alice:localhost",
|
||||
senderKey: senderIdentityKeys.curve25519,
|
||||
senderSigningKey: senderIdentityKeys.ed25519,
|
||||
p2pSession,
|
||||
recipient: "@alice:localhost",
|
||||
recipientCurve25519Key: aliceDeviceKeys.curve25519,
|
||||
recipientEd25519Key: aliceDeviceKeys.ed25519,
|
||||
plaincontent: {
|
||||
secret: testData.BACKUP_DECRYPTION_KEY_BASE64,
|
||||
name: "m.megolm_backup.v1",
|
||||
},
|
||||
plaintype: "io.element.msc4385.secret.push",
|
||||
});
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [secretPush],
|
||||
},
|
||||
};
|
||||
|
||||
const backupKeyReceivedPromise = new Promise<string>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupDecryptionKeyCached, resolve);
|
||||
});
|
||||
const keyBackupEnabledPromise = new Promise<void>((resolve) => {
|
||||
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
|
||||
if (enabled) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// alice should be using backup now
|
||||
expect(await backupKeyReceivedPromise).toBe(testData.SIGNED_BACKUP_DATA.version);
|
||||
await keyBackupEnabledPromise;
|
||||
expect(await crypto.getActiveSessionBackupVersion()).toBe(testData.SIGNED_BACKUP_DATA.version);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,9 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type CallLog } from "fetch-mock";
|
||||
import debug from "debug";
|
||||
|
||||
import { ClientEvent, createClient, type MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { ClientEvent, createClient, DebugLogger, type MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index";
|
||||
import { type RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { type AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
||||
@@ -27,7 +29,7 @@ import { emitPromise, EventCounter } from "../../test-utils/test-utils";
|
||||
|
||||
describe("Device dehydration", () => {
|
||||
it("should rehydrate and dehydrate a device", async () => {
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
@@ -38,6 +40,7 @@ describe("Device dehydration", () => {
|
||||
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||
},
|
||||
},
|
||||
logger: new DebugLogger(debug(`matrix-js-sdk:dehydration`)),
|
||||
});
|
||||
|
||||
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
|
||||
@@ -57,28 +60,35 @@ describe("Device dehydration", () => {
|
||||
});
|
||||
|
||||
const crypto = matrixClient.getCrypto()!;
|
||||
fetchMock.config.overwriteRoutes = true;
|
||||
|
||||
// start dehydration -- we start with no dehydrated device, and we
|
||||
// store the dehydrated device that we create
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
},
|
||||
});
|
||||
{ name: "get-dehydrated-device" },
|
||||
);
|
||||
let dehydratedDeviceBody: any;
|
||||
let dehydrationCount = 0;
|
||||
let resolveDehydrationPromise: () => void;
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||
dehydratedDeviceBody = JSON.parse(opts.body as string);
|
||||
dehydrationCount++;
|
||||
if (resolveDehydrationPromise) {
|
||||
resolveDehydrationPromise();
|
||||
}
|
||||
return {};
|
||||
});
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
(callLog) => {
|
||||
dehydratedDeviceBody = JSON.parse(callLog.options.body as string);
|
||||
dehydrationCount++;
|
||||
if (resolveDehydrationPromise) {
|
||||
resolveDehydrationPromise();
|
||||
}
|
||||
return {};
|
||||
},
|
||||
{ name: "put-dehydrated-device" },
|
||||
);
|
||||
await crypto.startDehydration();
|
||||
|
||||
expect(dehydrationCount).toEqual(1);
|
||||
@@ -89,7 +99,7 @@ describe("Device dehydration", () => {
|
||||
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
||||
resolveDehydrationPromise = resolve;
|
||||
});
|
||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await dehydrationPromise;
|
||||
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
@@ -99,16 +109,18 @@ describe("Device dehydration", () => {
|
||||
// restart dehydration -- rehydrate the device that we created above,
|
||||
// and create a new dehydrated device. We also set `createNewKey`, so
|
||||
// a new dehydration key will be set
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
device_id: dehydratedDeviceBody.device_id,
|
||||
device_data: dehydratedDeviceBody.device_data,
|
||||
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||
response: {
|
||||
device_id: dehydratedDeviceBody.device_id,
|
||||
device_data: dehydratedDeviceBody.device_data,
|
||||
},
|
||||
});
|
||||
const eventsResponse = jest.fn((url, opts) => {
|
||||
const eventsResponse = vi.fn((callLog: CallLog) => {
|
||||
// rehydrating should make two calls to the /events endpoint.
|
||||
// The first time will return a single event, and the second
|
||||
// time will return no events (which will signal to the
|
||||
// rehydration function that it can stop)
|
||||
const body = JSON.parse(opts.body as string);
|
||||
const body = JSON.parse(callLog.options.body as string);
|
||||
const nextBatch = body.next_batch ?? "0";
|
||||
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
|
||||
return {
|
||||
@@ -133,28 +145,30 @@ describe("Device dehydration", () => {
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
|
||||
|
||||
// test that if we get an error when we try to rotate, it emits an event
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
fetchMock.modifyRoute("put-dehydrated-device", {
|
||||
response: {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
},
|
||||
});
|
||||
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
|
||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
vi.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await rotationErrorEventPromise;
|
||||
|
||||
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
fetchMock.modifyRoute("get-dehydrated-device", {
|
||||
response: {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
},
|
||||
});
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||
return {};
|
||||
});
|
||||
fetchMock.modifyRoute("put-dehydrated-device", { response: { body: {} } });
|
||||
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
|
||||
await crypto.startDehydration(true);
|
||||
await rehydrationErrorEventPromise;
|
||||
@@ -179,11 +193,9 @@ async function initializeSecretStorage(
|
||||
const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
|
||||
const accountData: Map<string, object> = new Map();
|
||||
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
|
||||
const name = url.split("/").pop()!;
|
||||
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||
const name = callLog.url.split("/").pop()!;
|
||||
const value = accountData.get(name);
|
||||
if (value) {
|
||||
return value;
|
||||
@@ -197,9 +209,9 @@ async function initializeSecretStorage(
|
||||
};
|
||||
}
|
||||
});
|
||||
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
|
||||
const name = url.split("/").pop()!;
|
||||
const value = JSON.parse(opts.body as string);
|
||||
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (callLog) => {
|
||||
const name = callLog.url.split("/").pop()!;
|
||||
const value = JSON.parse(callLog.options.body as string);
|
||||
accountData.set(name, value);
|
||||
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
|
||||
return {};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { type Mocked } from "jest-mock";
|
||||
import { type Mocked } from "vitest";
|
||||
|
||||
import {
|
||||
createClient,
|
||||
encodeBase64,
|
||||
type IContent,
|
||||
type ICreateClientOpts,
|
||||
type IEvent,
|
||||
type IMegolmSessionData,
|
||||
@@ -36,7 +37,13 @@ import { advanceTimersUntil, awaitDecryption, syncPromise } from "../../test-uti
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { type KeyBackupInfo, type KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { decodeRecoveryKey, DecryptionFailureCode, CryptoEvent, type CryptoApi } from "../../../src/crypto-api";
|
||||
import {
|
||||
decodeRecoveryKey,
|
||||
DecryptionFailureCode,
|
||||
CryptoEvent,
|
||||
type CryptoApi,
|
||||
DecryptionKeyDoesNotMatchError,
|
||||
} from "../../../src/crypto-api";
|
||||
import { type KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||
|
||||
const ROOM_ID = testData.TEST_ROOM_ID;
|
||||
@@ -69,10 +76,11 @@ function mockUploadEmitter(
|
||||
expectedVersion: string,
|
||||
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
|
||||
const emitter = new TypedEventEmitter();
|
||||
fetchMock.removeRoute("mock-upload-emitter");
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
(url, request) => {
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
(callLog) => {
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version != expectedVersion) {
|
||||
return {
|
||||
status: 403,
|
||||
@@ -83,7 +91,7 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
}
|
||||
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
|
||||
const uploadPayload: KeyBackup = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
let count = 0;
|
||||
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
||||
for (const sessionId of Object.keys(value.sessions)) {
|
||||
@@ -99,9 +107,7 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
{ name: "mock-upload-emitter" },
|
||||
);
|
||||
return emitter;
|
||||
}
|
||||
@@ -117,12 +123,10 @@ describe("megolm-keys backup", () => {
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
beforeEach(async () => {
|
||||
// We want to use fake timers, but the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
mockInitialApiRequests(TEST_HOMESERVER_URL);
|
||||
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
|
||||
@@ -133,15 +137,12 @@ describe("megolm-keys backup", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (aliceClient !== undefined) {
|
||||
await aliceClient.stopClient();
|
||||
}
|
||||
aliceClient?.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
|
||||
@@ -207,9 +208,9 @@ describe("megolm-keys backup", () => {
|
||||
);
|
||||
|
||||
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (callLog) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version == "1") {
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
} else {
|
||||
@@ -237,11 +238,11 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
// Eventually, decryption succeeds.
|
||||
await awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
|
||||
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
|
||||
});
|
||||
|
||||
it("handles error on backup query gracefully", async () => {
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
@@ -253,9 +254,9 @@ describe("megolm-keys backup", () => {
|
||||
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
||||
await flushBackupRequest();
|
||||
|
||||
const calls = fetchMock.calls("getKey");
|
||||
const calls = fetchMock.callHistory.calls("getKey");
|
||||
expect(calls.length).toEqual(1);
|
||||
expect(calls[0][0]).toEqual(EXPECTED_URL);
|
||||
expect(calls[0].url).toEqual(EXPECTED_URL);
|
||||
|
||||
await flushBackupRequest();
|
||||
|
||||
@@ -274,11 +275,11 @@ describe("megolm-keys backup", () => {
|
||||
// Send Alice a message that she won't be able to decrypt
|
||||
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
|
||||
await flushBackupRequest();
|
||||
const calls = fetchMock.calls("getKey");
|
||||
const calls = fetchMock.callHistory.calls("getKey");
|
||||
expect(calls.length).toEqual(1);
|
||||
expect(calls[0][0]).toEqual(EXPECTED_URL);
|
||||
expect(calls[0].url).toEqual(EXPECTED_URL);
|
||||
|
||||
fetchMock.resetHistory();
|
||||
fetchMock.clearHistory();
|
||||
|
||||
// another message
|
||||
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
|
||||
@@ -288,7 +289,7 @@ describe("megolm-keys backup", () => {
|
||||
};
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse2);
|
||||
await flushBackupRequest();
|
||||
expect(fetchMock.calls("getKey").length).toEqual(0);
|
||||
expect(fetchMock.callHistory.calls("getKey").length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -364,9 +365,9 @@ describe("megolm-keys backup", () => {
|
||||
}
|
||||
|
||||
it("Should import full backup in chunks", async function () {
|
||||
const importMockImpl = jest.fn();
|
||||
const importMockImpl = vi.fn();
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
|
||||
// We need several rooms with several sessions to test chunking
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([45, 300, 345, 12, 130]);
|
||||
@@ -380,7 +381,7 @@ describe("megolm-keys backup", () => {
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const progressCallback = vi.fn();
|
||||
const result = await aliceCrypto.restoreKeyBackup({
|
||||
progressCallback,
|
||||
});
|
||||
@@ -417,7 +418,7 @@ describe("megolm-keys backup", () => {
|
||||
});
|
||||
|
||||
it("Should continue to process backup if a chunk import fails and report failures", async function () {
|
||||
const importMockImpl = jest
|
||||
const importMockImpl = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
// Fail to import first chunk
|
||||
@@ -427,7 +428,7 @@ describe("megolm-keys backup", () => {
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
jest.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
vi.spyOn(aliceCrypto.backupManager, "importBackedUpRoomKeys").mockImplementation(importMockImpl);
|
||||
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([100, 300]);
|
||||
|
||||
@@ -439,7 +440,7 @@ describe("megolm-keys backup", () => {
|
||||
check!.backupInfo!.version!,
|
||||
);
|
||||
|
||||
const progressCallback = jest.fn();
|
||||
const progressCallback = vi.fn();
|
||||
const result = await aliceCrypto.restoreKeyBackup({ progressCallback });
|
||||
|
||||
expect(result.total).toStrictEqual(expectedTotal);
|
||||
@@ -463,13 +464,13 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
it("Should continue if some keys fails to decrypt", async function () {
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.importBackedUpRoomKeys = jest.fn();
|
||||
aliceCrypto.importBackedUpRoomKeys = vi.fn();
|
||||
|
||||
const decryptionFailureCount = 2;
|
||||
|
||||
const mockDecryptor = {
|
||||
// DecryptSessions does not reject on decryption failure, but just skip the key
|
||||
decryptSessions: jest.fn().mockImplementation((sessions) => {
|
||||
decryptSessions: vi.fn().mockImplementation((sessions) => {
|
||||
// simulate fail to decrypt 2 keys out of all
|
||||
const decrypted = [];
|
||||
const keys = Object.keys(sessions);
|
||||
@@ -480,11 +481,11 @@ describe("megolm-keys backup", () => {
|
||||
}
|
||||
return decrypted;
|
||||
}),
|
||||
free: jest.fn(),
|
||||
free: vi.fn(),
|
||||
};
|
||||
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
aliceCrypto.getBackupDecryptor = jest.fn().mockResolvedValue(mockDecryptor);
|
||||
aliceCrypto.getBackupDecryptor = vi.fn().mockResolvedValue(mockDecryptor);
|
||||
|
||||
const { response, expectedTotal } = createBackupDownloadResponse([100]);
|
||||
|
||||
@@ -505,17 +506,12 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
it("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);
|
||||
vi.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const fullBackup = createFullBackup(
|
||||
testData.MEGOLM_SESSION_DATA.session_id,
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
await aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
||||
@@ -526,15 +522,44 @@ describe("megolm-keys backup", () => {
|
||||
expect(result.imported).toStrictEqual(1);
|
||||
});
|
||||
|
||||
it("Should throw an error if the decryption key does not match the backup", async function () {
|
||||
// Given the stored backup decryption key does not match the public backup info
|
||||
// @ts-ignore - mock a private method for testing purpose
|
||||
vi.spyOn(aliceCrypto.secretStorage, "get").mockResolvedValue(testData.BACKUP_DECRYPTION_KEY_BASE64_ALT);
|
||||
|
||||
const fullBackup = createFullBackup(
|
||||
testData.MEGOLM_SESSION_DATA.session_id,
|
||||
testData.CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
|
||||
|
||||
// When we load that key, we throw because the keys don't match
|
||||
await expect(aliceCrypto.loadSessionBackupPrivateKeyFromSecretStorage()).rejects.toThrow(
|
||||
DecryptionKeyDoesNotMatchError,
|
||||
);
|
||||
});
|
||||
|
||||
it("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");
|
||||
});
|
||||
|
||||
function createFullBackup(sessionId: string, data: KeyBackupSession) {
|
||||
return {
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[sessionId]: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
describe("backupLoop", () => {
|
||||
it("Alice should upload known keys when backup is enabled", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404, { name: "room-keys-version" });
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -571,8 +596,8 @@ describe("megolm-keys backup", () => {
|
||||
});
|
||||
});
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
fetchMock.modifyRoute("room-keys-version", {
|
||||
response: { status: 200, body: testData.SIGNED_BACKUP_DATA },
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
@@ -581,7 +606,7 @@ describe("megolm-keys backup", () => {
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
|
||||
@@ -605,7 +630,7 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
await newKeyUploadPromise;
|
||||
});
|
||||
|
||||
@@ -630,7 +655,7 @@ describe("megolm-keys backup", () => {
|
||||
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
name: "room-keys-version",
|
||||
});
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
@@ -640,7 +665,7 @@ describe("megolm-keys backup", () => {
|
||||
await aliceCrypto.importRoomKeys(someRoomKeys);
|
||||
|
||||
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
// wait for all keys to be backed up
|
||||
await remainingZeroPromise;
|
||||
@@ -651,10 +676,7 @@ describe("megolm-keys backup", () => {
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
// Let's simulate that a new backup is available by returning error code on key upload
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
|
||||
|
||||
// If we import a new key the loop will try to upload to old version, it will
|
||||
// fail then check the current version and switch if trusted
|
||||
@@ -697,12 +719,12 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
await aliceCrypto.importRoomKeys([newKey]);
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
await disableOldBackup;
|
||||
await enableNewBackup;
|
||||
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
await newKeyUploadPromise;
|
||||
@@ -717,22 +739,14 @@ describe("megolm-keys backup", () => {
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// on the first key upload attempt, simulate a network failure
|
||||
const failurePromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
throw new TypeError(`Failed to fetch`);
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
|
||||
resolve(undefined);
|
||||
throw new TypeError(`Failed to fetch`);
|
||||
});
|
||||
});
|
||||
|
||||
// kick the import loop off and wait for the failed request
|
||||
@@ -741,27 +755,21 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
jest.advanceTimersByTime(10 * 60 * 1000);
|
||||
vi.advanceTimersByTime(10 * 60 * 1000);
|
||||
await failurePromise;
|
||||
|
||||
// Fix the endpoint to do successful uploads
|
||||
const successPromise = new Promise((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
() => {
|
||||
resolve(undefined);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 2,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.putOnce("path:/_matrix/client/v3/room_keys/keys", () => {
|
||||
resolve(undefined);
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
count: 2,
|
||||
etag: "abcdefg",
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
|
||||
@@ -774,7 +782,7 @@ describe("megolm-keys backup", () => {
|
||||
});
|
||||
|
||||
// run the timers, which will make the backup loop redo the request
|
||||
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
|
||||
await vi.advanceTimersByTimeAsync(10 * 60 * 1000);
|
||||
await successPromise;
|
||||
await allKeysUploadedPromise;
|
||||
});
|
||||
@@ -782,7 +790,7 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
it("getActiveSessionBackupVersion() should give correct result", async function () {
|
||||
// 404 means that there is no active backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", 404);
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -801,9 +809,7 @@ describe("megolm-keys backup", () => {
|
||||
// Serve a backup with no trusted signature
|
||||
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", unsignedBackup);
|
||||
|
||||
const checked = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
|
||||
@@ -813,9 +819,7 @@ describe("megolm-keys backup", () => {
|
||||
expect(backupStatus).toBeNull();
|
||||
|
||||
// Add a valid signature to the backup
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// check that signalling is working
|
||||
const backupPromise = new Promise<void>((resolve, reject) => {
|
||||
@@ -837,7 +841,7 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
it("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.getOnce("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();
|
||||
@@ -853,11 +857,9 @@ describe("megolm-keys 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,
|
||||
});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toStrictEqual(testData.SIGNED_BACKUP_DATA);
|
||||
expect(await aliceCrypto.getKeyBackupInfo()).toMatchObject(testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
// Delete the backup and we are expecting the key backup to be disabled
|
||||
const keyBackupStatus = Promise.withResolvers<boolean>();
|
||||
@@ -991,7 +993,7 @@ describe("megolm-keys backup", () => {
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
@@ -1001,9 +1003,7 @@ describe("megolm-keys backup", () => {
|
||||
delete unsignedBackup.auth_data.signatures;
|
||||
unsignedBackup.version = "2";
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", unsignedBackup);
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
@@ -1018,7 +1018,7 @@ describe("megolm-keys backup", () => {
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
@@ -1028,9 +1028,7 @@ describe("megolm-keys backup", () => {
|
||||
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
|
||||
newBackup.version = newBackupVersion;
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", newBackup);
|
||||
|
||||
await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
|
||||
@@ -1045,25 +1043,19 @@ describe("megolm-keys backup", () => {
|
||||
await waitForDeviceList();
|
||||
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
|
||||
|
||||
const result = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(result).toBeTruthy();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
|
||||
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
fetchMock.getOnce("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No backup found",
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
|
||||
expect(noResult).toBeNull();
|
||||
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
|
||||
@@ -1072,10 +1064,12 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
describe("Backup Changed from other sessions", () => {
|
||||
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, {
|
||||
name: "room-keys-version",
|
||||
});
|
||||
|
||||
// ignore requests to send room key requests
|
||||
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
fetchMock.getOnce("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
|
||||
|
||||
aliceClient = await initTestClient();
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
@@ -1108,9 +1102,9 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
(url, request) => {
|
||||
(callLog) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version == "1") {
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
} else {
|
||||
@@ -1124,7 +1118,7 @@ describe("megolm-keys backup", () => {
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
{ name: "room-keys" },
|
||||
);
|
||||
|
||||
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
|
||||
@@ -1135,7 +1129,7 @@ describe("megolm-keys backup", () => {
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
|
||||
|
||||
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
|
||||
expect(event.getContent<IContent>()).toEqual(testData.CLEAR_EVENT.content);
|
||||
|
||||
// =====
|
||||
// Second suppose now that the backup has changed to version 2
|
||||
@@ -1146,7 +1140,7 @@ describe("megolm-keys backup", () => {
|
||||
version: "2",
|
||||
};
|
||||
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, { overwriteRoutes: true });
|
||||
fetchMock.modifyRoute("room-keys-version", { response: newBackup });
|
||||
// suppose the new key is now known
|
||||
const aliceCrypto = aliceClient.getCrypto()!;
|
||||
await aliceCrypto.storeSessionBackupPrivateKey(
|
||||
@@ -1159,11 +1153,10 @@ describe("megolm-keys backup", () => {
|
||||
|
||||
const awaitHasQueriedNewBackup: PromiseWithResolvers<void> = Promise.withResolvers<void>();
|
||||
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
|
||||
(url, request) => {
|
||||
fetchMock.modifyRoute("room-keys", {
|
||||
response: (callLog) => {
|
||||
// check that the version is correct
|
||||
const version = new URLSearchParams(new URL(url).search).get("version");
|
||||
const version = new URLSearchParams(new URL(callLog.url).search).get("version");
|
||||
if (version == newBackup.version) {
|
||||
awaitHasQueriedNewBackup.resolve();
|
||||
return testData.CURVE25519_KEY_BACKUP_DATA;
|
||||
@@ -1179,8 +1172,7 @@ describe("megolm-keys backup", () => {
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
});
|
||||
|
||||
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the new backup.
|
||||
const newMessage: Partial<IEvent> = {
|
||||
@@ -1216,7 +1208,7 @@ describe("megolm-keys backup", () => {
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the dummy device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
|
||||
@@ -16,6 +16,8 @@ limitations under the License.
|
||||
|
||||
import Olm from "@matrix-org/olm";
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
|
||||
import {
|
||||
type IContent,
|
||||
@@ -30,6 +32,7 @@ import { type IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { type ISyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { syncPromise } from "../../test-utils/test-utils";
|
||||
import { type KeyBackupInfo } from "../../../src/crypto-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
|
||||
/**
|
||||
* @module
|
||||
@@ -93,15 +96,15 @@ export function bootstrapCrossSigningTestOlmAccount(
|
||||
deviceId: string,
|
||||
keyBackupInfo: KeyBackupInfo[] = [],
|
||||
): Partial<IDownloadKeyResult> {
|
||||
const olmAliceMSK = new globalThis.Olm.PkSigning();
|
||||
const olmAliceMSK = new Olm.PkSigning();
|
||||
const masterPrivkey = olmAliceMSK.generate_seed();
|
||||
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
|
||||
|
||||
const olmAliceUSK = new globalThis.Olm.PkSigning();
|
||||
const olmAliceUSK = new Olm.PkSigning();
|
||||
const userPrivkey = olmAliceUSK.generate_seed();
|
||||
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
|
||||
|
||||
const olmAliceSSK = new globalThis.Olm.PkSigning();
|
||||
const olmAliceSSK = new Olm.PkSigning();
|
||||
const sskPrivkey = olmAliceSSK.generate_seed();
|
||||
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
|
||||
|
||||
@@ -189,7 +192,7 @@ export async function createOlmSession(
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new globalThis.Olm.Session();
|
||||
const session = new Olm.Session();
|
||||
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
|
||||
return session;
|
||||
}
|
||||
@@ -302,6 +305,9 @@ export function encryptMegolmEventRawPlainText(opts: {
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
unsigned: {},
|
||||
state_key: opts.plaintext.hasOwnProperty("state_key")
|
||||
? `${opts.plaintext.type}:${opts.plaintext.state_key}`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -414,3 +420,128 @@ export async function establishOlmSession(
|
||||
await syncPromise(testClient);
|
||||
return p2pSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client shares keys with the given recipient
|
||||
*
|
||||
* Waits for an HTTP request to send the encrypted m.room_key to-device message; decrypts it and uses it
|
||||
* to establish an Olm InboundGroupSession.
|
||||
*
|
||||
* @param recipientUserID - the user id of the expected recipient
|
||||
*
|
||||
* @param recipientOlmAccount - Olm.Account for the recipient
|
||||
*
|
||||
* @param recipientOlmSession - an Olm.Session for the recipient, which must already have exchanged pre-key
|
||||
* messages with the sender. Alternatively, null, in which case we will expect a pre-key message.
|
||||
*
|
||||
* @returns the established inbound group session
|
||||
*/
|
||||
export async function expectSendRoomKey(
|
||||
recipientUserID: string,
|
||||
recipientOlmAccount: Olm.Account,
|
||||
recipientOlmSession: Olm.Session | null = null,
|
||||
): Promise<Olm.InboundGroupSession> {
|
||||
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
|
||||
|
||||
function onSendRoomKey(content: any): Olm.InboundGroupSession {
|
||||
const m = content.messages[recipientUserID].DEVICE_ID;
|
||||
const ct = m.ciphertext[testRecipientKey];
|
||||
|
||||
if (!recipientOlmSession) {
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
recipientOlmSession = new Olm.Session();
|
||||
recipientOlmSession.create_inbound(recipientOlmAccount, ct.body);
|
||||
} else {
|
||||
expect(ct.type).toEqual(1); // regular message
|
||||
}
|
||||
|
||||
const decrypted = JSON.parse(recipientOlmSession.decrypt(ct.type, ct.body));
|
||||
expect(decrypted.type).toEqual("m.room_key");
|
||||
const inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return inboundGroupSession;
|
||||
}
|
||||
return await new Promise<Olm.InboundGroupSession>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(onSendRoomKey(content));
|
||||
return {};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the event received on rooms/{roomId}/send/m.room.encrypted endpoint.
|
||||
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
|
||||
* @returns the content of the encrypted event
|
||||
*/
|
||||
export function expectEncryptedSendMessageEvent() {
|
||||
return new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return { event_id: "$event_id" };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the event received on rooms/{roomId}/state/m.room.encrypted/{stateKey} endpoint.
|
||||
* See https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
|
||||
* @returns the content of the encrypted event
|
||||
*/
|
||||
function expectEncryptedSendStateEvent() {
|
||||
return new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/state/m.room.encrypted/"), (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
resolve(content);
|
||||
return { event_id: "$event_id" };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client sends an encrypted message event
|
||||
*
|
||||
* Waits for an HTTP request to send an encrypted message in the test room.
|
||||
*
|
||||
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
|
||||
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
|
||||
*
|
||||
* @returns The content of the successfully-decrypted event
|
||||
*/
|
||||
export async function expectSendMegolmMessageEvent(
|
||||
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
|
||||
): Promise<Partial<IEvent>> {
|
||||
const encryptedMessageContent = await expectEncryptedSendMessageEvent();
|
||||
|
||||
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
|
||||
const inboundGroupSession = await inboundGroupSessionPromise;
|
||||
|
||||
const r: any = inboundGroupSession.decrypt(encryptedMessageContent!.ciphertext);
|
||||
logger.log("Decrypted received megolm message", r);
|
||||
return JSON.parse(r.plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect that the client sends an encrypted state event
|
||||
*
|
||||
* Waits for an HTTP request to send an encrypted state event in the test room.
|
||||
*
|
||||
* @param inboundGroupSessionPromise - a promise for an Olm InboundGroupSession, which will
|
||||
* be used to decrypt the event. We will wait for this to resolve once the HTTP request has been processed.
|
||||
*
|
||||
* @returns The content of the successfully-decrypted state event
|
||||
*/
|
||||
export async function expectSendMegolmStateEvent(
|
||||
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
|
||||
): Promise<Partial<IEvent>> {
|
||||
const encryptedStateContent = await expectEncryptedSendStateEvent();
|
||||
|
||||
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
|
||||
const inboundGroupSession = await inboundGroupSessionPromise;
|
||||
|
||||
const r: any = inboundGroupSession.decrypt(encryptedStateContent!.ciphertext);
|
||||
logger.log("Decrypted received megolm state event", r);
|
||||
return JSON.parse(r.plaintext);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { createClient, IndexedDBCryptoStore } from "../../../src";
|
||||
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
|
||||
@@ -26,7 +26,7 @@ import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostor
|
||||
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
|
||||
import { CryptoEvent } from "../../../src/crypto-api";
|
||||
|
||||
jest.setTimeout(15000);
|
||||
vi.setConfig({ testTimeout: 15000 });
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -122,6 +122,7 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should ignore a second call", async () => {
|
||||
const matrixClient = createClient({
|
||||
baseUrl: "http://test.server",
|
||||
@@ -134,10 +135,6 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
});
|
||||
|
||||
describe("Libolm Migration", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it("should migrate from libolm", async () => {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
|
||||
|
||||
@@ -155,7 +152,7 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
|
||||
});
|
||||
|
||||
const progressListener = jest.fn();
|
||||
const progressListener = vi.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
@@ -326,7 +323,7 @@ describe("MatrixClient.initRustCrypto", () => {
|
||||
});
|
||||
|
||||
// When we start Rust crypto, potentially triggering an upgrade
|
||||
const progressListener = jest.fn();
|
||||
const progressListener = vi.fn();
|
||||
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
|
||||
|
||||
await matrixClient.initRustCrypto();
|
||||
@@ -478,6 +475,7 @@ describe("MatrixClient.clearStores", () => {
|
||||
expect(await indexedDB.databases()).toHaveLength(0);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should not fail in environments without indexedDB", async () => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = undefined!;
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
Copyright 2025 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 anotherjson from "another-json";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import { TEST_ROOM_ID as ROOM_ID } from "../../test-utils/test-data";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
createClient,
|
||||
HistoryVisibility,
|
||||
PendingEventOrdering,
|
||||
type IStartClientOpts,
|
||||
type MatrixClient,
|
||||
} from "../../../src/matrix";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import {
|
||||
createOlmAccount,
|
||||
createOlmSession,
|
||||
encryptGroupSessionKey,
|
||||
encryptMegolmEvent,
|
||||
getTestOlmAccountKeys,
|
||||
expectSendRoomKey,
|
||||
expectSendMegolmStateEvent,
|
||||
} from "./olm-utils";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
|
||||
describe("Encrypted State Events", () => {
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
let testSenderKey = "";
|
||||
|
||||
/** the MatrixClient under test */
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} to catch the uploaded keys */
|
||||
let keyReceiver: E2EKeyReceiver;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
let syncResponder: ISyncResponder;
|
||||
|
||||
async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise<void> {
|
||||
logger.log(aliceClient.getUserId() + ": starting");
|
||||
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
syncResponder.sendOrQueueSyncResponse({ next_batch: 1 });
|
||||
|
||||
aliceClient.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
...opts,
|
||||
});
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
logger.log(aliceClient.getUserId() + ": started");
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.catch(404);
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: "@alice:localhost",
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "xzcvb",
|
||||
logger: logger.getChild("aliceClient"),
|
||||
enableEncryptedStateEvents: true,
|
||||
});
|
||||
|
||||
keyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
await aliceClient.initRustCrypto();
|
||||
|
||||
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
|
||||
testOlmAccount = await createOlmAccount();
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
}, 10000);
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
});
|
||||
|
||||
function expectAliceKeyQuery(response: any) {
|
||||
fetchMock.postOnce(new RegExp("/keys/query"), (callLog) => response);
|
||||
}
|
||||
|
||||
function expectAliceKeyClaim(response: any) {
|
||||
fetchMock.postOnce(new RegExp("/keys/claim"), response);
|
||||
}
|
||||
|
||||
function getTestKeysClaimResponse(userId: string) {
|
||||
testOlmAccount.generate_one_time_keys(1);
|
||||
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
const keyId = Object.keys(testOneTimeKeys.curve25519)[0];
|
||||
const oneTimeKey: string = testOneTimeKeys.curve25519[keyId];
|
||||
const unsignedKeyResult = { key: oneTimeKey };
|
||||
const j = anotherjson.stringify(unsignedKeyResult);
|
||||
const sig = testOlmAccount.sign(j);
|
||||
const keyResult = {
|
||||
...unsignedKeyResult,
|
||||
signatures: { [userId]: { "ed25519:DEVICE_ID": sig } },
|
||||
};
|
||||
|
||||
return {
|
||||
one_time_keys: { [userId]: { DEVICE_ID: { ["signed_curve25519:" + keyId]: keyResult } } },
|
||||
failures: {},
|
||||
};
|
||||
}
|
||||
|
||||
it("Should receive an encrypted state event", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
recipient: aliceClient.getUserId()!,
|
||||
recipientCurve25519Key: keyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: keyReceiver.getSigningKey(),
|
||||
olmAccount: testOlmAccount,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a state event with the group session
|
||||
const eventEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
plaintext: {
|
||||
type: "m.room.topic",
|
||||
state_key: "",
|
||||
content: {
|
||||
topic: "Secret!",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: { timeline: { events: [eventEncrypted] } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const room = aliceClient.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
|
||||
// it probably won't be decrypted yet, because it takes a while to process the olm keys
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true });
|
||||
expect(decryptedEvent.getContent().topic).toEqual("Secret!");
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Should send an encrypted state event", async () => {
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
const keyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
keyResponder.addKeyReceiver("@alice:localhost", keyReceiver);
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
keyResponder.addDeviceKeys(testDeviceKeys);
|
||||
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// Alice shares a room with Bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], HistoryVisibility.Joined, ROOM_ID, true));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and claim one of Bob's OTKs ...
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
|
||||
|
||||
// ... and send an m.room.topic message
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
|
||||
await Promise.all([
|
||||
aliceClient.setRoomTopic(ROOM_ID, "Secret!"),
|
||||
expectSendMegolmStateEvent(inboundGroupSessionPromise),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -14,16 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import { createClient, type MatrixClient } from "../../../src";
|
||||
import {
|
||||
ClientEvent,
|
||||
createClient,
|
||||
type IToDeviceEvent,
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
type ReceivedToDeviceMessage,
|
||||
} from "../../../src";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { encryptOlmEvent, establishOlmSession, getTestOlmAccountKeys } from "./olm-utils.ts";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -43,12 +52,13 @@ describe("to-device-messages", () => {
|
||||
|
||||
/** an object which intercepts `/keys/query` requests on the test homeserver */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
let e2eKeyReceiver: E2EKeyReceiver;
|
||||
let syncResponder: SyncResponder;
|
||||
|
||||
beforeEach(
|
||||
async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://server.com";
|
||||
aliceClient = createClient({
|
||||
@@ -59,8 +69,8 @@ describe("to-device-messages", () => {
|
||||
});
|
||||
|
||||
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
|
||||
new E2EKeyReceiver(homeserverUrl);
|
||||
const syncResponder = new SyncResponder(homeserverUrl);
|
||||
e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
|
||||
syncResponder = new SyncResponder(homeserverUrl);
|
||||
|
||||
// add bob as known user
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([testData.BOB_TEST_USER_ID]));
|
||||
@@ -89,7 +99,6 @@ describe("to-device-messages", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("encryptToDeviceMessages", () => {
|
||||
@@ -149,4 +158,111 @@ describe("to-device-messages", () => {
|
||||
// for future: check that bob's device can decrypt the ciphertext?
|
||||
});
|
||||
});
|
||||
|
||||
describe("receive to-device-messages", () => {
|
||||
it("Should receive decrypted to-device message via ClientEvent", async () => {
|
||||
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
|
||||
await Olm.init();
|
||||
const testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
|
||||
const testDeviceKeys = getTestOlmAccountKeys(testOlmAccount, "@bob:xyz", "DEVICE_ID");
|
||||
e2eKeyResponder.addDeviceKeys(testDeviceKeys);
|
||||
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const p2pSession = await establishOlmSession(aliceClient, e2eKeyReceiver, syncResponder, testOlmAccount);
|
||||
|
||||
const toDeviceEvent = encryptOlmEvent({
|
||||
sender: "@bob:xyz",
|
||||
senderKey: testDeviceKeys.keys[`curve25519:DEVICE_ID`],
|
||||
senderSigningKey: testDeviceKeys.keys[`ed25519:DEVICE_ID`],
|
||||
p2pSession: p2pSession,
|
||||
recipient: aliceClient.getUserId()!,
|
||||
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
|
||||
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
|
||||
plaincontent: {
|
||||
body: "foo",
|
||||
},
|
||||
plaintype: "m.test.type",
|
||||
});
|
||||
|
||||
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
|
||||
processedToDeviceResolver.resolve(payload);
|
||||
});
|
||||
|
||||
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
|
||||
oldToDeviceResolver.resolve(event);
|
||||
});
|
||||
|
||||
expect(toDeviceEvent.type).toBe("m.room.encrypted");
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
|
||||
|
||||
expect(message.type).toBe("m.test.type");
|
||||
expect(message.content["body"]).toBe("foo");
|
||||
|
||||
expect(encryptionInfo).not.toBeNull();
|
||||
expect(encryptionInfo!.senderVerified).toBe(false);
|
||||
expect(encryptionInfo!.sender).toBe("@bob:xyz");
|
||||
expect(encryptionInfo!.senderDevice).toBe("DEVICE_ID");
|
||||
|
||||
const oldFormat = await oldToDeviceResolver.promise;
|
||||
expect(oldFormat.isEncrypted()).toBe(true);
|
||||
expect(oldFormat.getType()).toBe("m.test.type");
|
||||
expect(oldFormat.getContent()["body"]).toBe("foo");
|
||||
});
|
||||
|
||||
it("Should receive clear to-device message via ClientEvent", async () => {
|
||||
await aliceClient.startClient();
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const toDeviceEvent: IToDeviceEvent = {
|
||||
sender: "@bob:xyz",
|
||||
type: "m.test.type",
|
||||
content: {
|
||||
body: "foo",
|
||||
},
|
||||
};
|
||||
|
||||
const processedToDeviceResolver: PromiseWithResolvers<ReceivedToDeviceMessage> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ReceivedToDeviceMessage, (payload) => {
|
||||
processedToDeviceResolver.resolve(payload);
|
||||
});
|
||||
|
||||
const oldToDeviceResolver: PromiseWithResolvers<MatrixEvent> = Promise.withResolvers();
|
||||
|
||||
aliceClient.on(ClientEvent.ToDeviceEvent, (event) => {
|
||||
oldToDeviceResolver.resolve(event);
|
||||
});
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [toDeviceEvent] } });
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
const { message, encryptionInfo } = await processedToDeviceResolver.promise;
|
||||
|
||||
expect(message.type).toBe("m.test.type");
|
||||
expect(message.content["body"]).toBe("foo");
|
||||
|
||||
// When the message is not encrypted, we don't have the encryptionInfo.
|
||||
expect(encryptionInfo).toBeNull();
|
||||
|
||||
const oldFormat = await oldToDeviceResolver.promise;
|
||||
expect(oldFormat.isEncrypted()).toBe(false);
|
||||
expect(oldFormat.getType()).toBe("m.test.type");
|
||||
expect(oldFormat.getContent()["body"]).toBe("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,14 +17,16 @@ limitations under the License.
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import debug from "debug";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { createHash } from "crypto";
|
||||
import Olm from "@matrix-org/olm";
|
||||
|
||||
import type FetchMock from "fetch-mock";
|
||||
import {
|
||||
createClient,
|
||||
DebugLogger,
|
||||
DeviceVerification,
|
||||
type IContent,
|
||||
type ICreateClientOpts,
|
||||
@@ -44,8 +46,8 @@ import {
|
||||
type Verifier,
|
||||
VerifierEvent,
|
||||
} from "../../../src/crypto-api/verification";
|
||||
import { escapeRegExp } from "../../../src/utils";
|
||||
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import { escapeRegExp, sleep } from "../../../src/utils";
|
||||
import { awaitDecryption, emitPromise, getSyncResponse, syncPromise, waitFor } from "../../test-utils/test-utils";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import {
|
||||
BACKUP_DECRYPTION_KEY_BASE64,
|
||||
@@ -77,14 +79,9 @@ import {
|
||||
import { type KeyBackupInfo, CryptoEvent } from "../../../src/crypto-api";
|
||||
import { encodeBase64 } from "../../../src/base64";
|
||||
|
||||
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
|
||||
// to ensure that we don't end up with dangling timeouts.
|
||||
// But the wasm bindings of matrix-sdk-crypto rely on a working `queueMicrotask`.
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
beforeAll(async () => {
|
||||
// we use the libolm primitives in the test, so init the Olm library
|
||||
await globalThis.Olm.init();
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
// load the rust library. This can take a few seconds on a slow GH worker.
|
||||
@@ -94,6 +91,10 @@ beforeAll(async () => {
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
}, 10000);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
@@ -110,7 +111,6 @@ const TEST_HOMESERVER_URL = "https://alice-server.com";
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
// we test with both crypto stacks...
|
||||
describe("verification", () => {
|
||||
/** the client under test */
|
||||
let aliceClient: MatrixClient;
|
||||
@@ -127,7 +127,6 @@ describe("verification", () => {
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
|
||||
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
|
||||
@@ -138,14 +137,12 @@ describe("verification", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (aliceClient !== undefined) {
|
||||
await aliceClient.stopClient();
|
||||
}
|
||||
aliceClient?.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
if (vi.isFakeTimers()) {
|
||||
await vi.runAllTimersAsync();
|
||||
}
|
||||
});
|
||||
|
||||
describe("Outgoing verification requests for another device", () => {
|
||||
@@ -153,11 +150,10 @@ describe("verification", () => {
|
||||
// pretend that we have another device, which we will verify
|
||||
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
|
||||
|
||||
fetchMock.put(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`),
|
||||
{ ok: false, status: 404 },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`), {
|
||||
ok: false,
|
||||
status: 404,
|
||||
});
|
||||
});
|
||||
|
||||
// test with (1) the default verification method list, (2) a custom verification method list.
|
||||
@@ -209,7 +205,7 @@ describe("verification", () => {
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
if (methods !== undefined) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
// eslint-disable-next-line @vitest/no-conditional-expect
|
||||
expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
|
||||
}
|
||||
|
||||
@@ -242,7 +238,7 @@ describe("verification", () => {
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
vi.advanceTimersByTime(10);
|
||||
|
||||
requestBody = await sendToDevicePromise;
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
@@ -254,7 +250,7 @@ describe("verification", () => {
|
||||
|
||||
// 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 globalThis.Olm.SAS();
|
||||
const olmSAS = new Olm.SAS();
|
||||
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
@@ -320,7 +316,7 @@ describe("verification", () => {
|
||||
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// And now Alice starts a SAS verification
|
||||
let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||
@@ -348,7 +344,7 @@ describe("verification", () => {
|
||||
|
||||
// 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 globalThis.Olm.SAS();
|
||||
const olmSAS = new Olm.SAS();
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
|
||||
|
||||
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
|
||||
@@ -513,7 +509,7 @@ describe("verification", () => {
|
||||
// Rust crypto waits for the 'done' to arrive from the other side.
|
||||
if (request.phase === VerificationPhase.Done) {
|
||||
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
// eslint-disable-next-line @vitest/no-conditional-expect
|
||||
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
|
||||
await verificationPromise;
|
||||
}
|
||||
@@ -636,7 +632,7 @@ describe("verification", () => {
|
||||
expect(request.verifier).toBeUndefined();
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// ... but Alice wants to do an SAS verification
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
|
||||
@@ -681,7 +677,7 @@ describe("verification", () => {
|
||||
expect(request.verifier).toBeUndefined();
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// ... but the dummy device wants to do an SAS verification
|
||||
returnToDeviceMessageFromSync(buildSasStartMessage(transactionId));
|
||||
@@ -734,6 +730,35 @@ describe("verification", () => {
|
||||
expect(request.cancellingUserId).toEqual("@alice:localhost");
|
||||
});
|
||||
|
||||
it("does not include cancelled requests in the list of requests", async () => {
|
||||
// Given Alice started a verification request
|
||||
const [, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
|
||||
]);
|
||||
const transactionId = request.transactionId!;
|
||||
|
||||
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
|
||||
await waitForVerificationRequestChanged(request);
|
||||
|
||||
// Sanity: the request is listed
|
||||
const requestsBeforeCancel = aliceClient
|
||||
.getCrypto()!
|
||||
.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
|
||||
|
||||
expect(requestsBeforeCancel).toHaveLength(1);
|
||||
|
||||
// When Alice cancels it
|
||||
await Promise.all([expectSendToDeviceMessage("m.key.verification.cancel"), request.cancel()]);
|
||||
|
||||
// Then it is no longer listed as in progress
|
||||
const requestsAfterCancel = aliceClient
|
||||
.getCrypto()!
|
||||
.getVerificationRequestsToDeviceInProgress(TEST_USER_ID);
|
||||
|
||||
expect(requestsAfterCancel).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can cancel during the SAS phase", async () => {
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
const [, request] = await Promise.all([
|
||||
@@ -760,7 +785,7 @@ describe("verification", () => {
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
vi.advanceTimersByTime(10);
|
||||
await sendToDevicePromise;
|
||||
|
||||
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
|
||||
@@ -905,19 +930,16 @@ describe("verification", () => {
|
||||
function awaitRoomMessageRequest(): Promise<IContent> {
|
||||
return new Promise((resolve) => {
|
||||
// Case of unencrypted message of the new crypto
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId",
|
||||
(url: string, options: RequestInit) => {
|
||||
resolve(JSON.parse(options.body as string));
|
||||
return { event_id: "$YUwRidLecu:example.com" };
|
||||
},
|
||||
);
|
||||
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.room.message/:txId", (callLog) => {
|
||||
resolve(JSON.parse(callLog.options.body as string));
|
||||
return { event_id: "$YUwRidLecu:example.com" };
|
||||
});
|
||||
|
||||
// Case of encrypted message of the old crypto
|
||||
fetchMock.put(
|
||||
"express:/_matrix/client/v3/rooms/:roomId/send/m.room.encrypted/:txId",
|
||||
async (url: string, options: RequestInit) => {
|
||||
const encryptedMessage = JSON.parse(options.body as string);
|
||||
async (callLog) => {
|
||||
const encryptedMessage = JSON.parse(callLog.options.body as string);
|
||||
const event = new MatrixEvent({
|
||||
content: encryptedMessage,
|
||||
type: "m.room.encrypted",
|
||||
@@ -940,7 +962,7 @@ describe("verification", () => {
|
||||
|
||||
// In `DeviceList#doQueuedQueries`, the key download response is processed every 5ms
|
||||
// 5ms by users, ie Bob and Alice
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
const messageRequestPromise = awaitRoomMessageRequest();
|
||||
const verificationRequest = await aliceClient
|
||||
@@ -1050,7 +1072,14 @@ describe("verification", () => {
|
||||
});
|
||||
|
||||
it("ignores old verification requests", async () => {
|
||||
const eventHandler = jest.fn();
|
||||
const debug = vi.fn();
|
||||
const info = vi.fn();
|
||||
const warn = vi.fn();
|
||||
|
||||
// @ts-ignore overriding RustCrypto's logger
|
||||
aliceClient.getCrypto()!.logger = { debug, info, warn };
|
||||
|
||||
const eventHandler = vi.fn();
|
||||
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
|
||||
|
||||
const verificationRequestEvent = createVerificationRequestEvent();
|
||||
@@ -1064,6 +1093,16 @@ describe("verification", () => {
|
||||
const matrixEvent = room.getLiveTimeline().getEvents()[0];
|
||||
expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id);
|
||||
|
||||
// Wait until the request has been processed. We use a real sleep()
|
||||
// here to make sure any background async tasks are completed.
|
||||
vi.useRealTimers();
|
||||
await waitFor(async () => {
|
||||
expect(info).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^Ignoring just-received verification request/),
|
||||
);
|
||||
sleep(100);
|
||||
});
|
||||
|
||||
// check that an event has not been raised, and that the request is not found
|
||||
expect(eventHandler).not.toHaveBeenCalled();
|
||||
expect(
|
||||
@@ -1071,6 +1110,29 @@ describe("verification", () => {
|
||||
).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("ignores cancelled verification requests", async () => {
|
||||
// Given a verification request exists
|
||||
const event = createVerificationRequestEvent();
|
||||
returnRoomMessageFromSync(TEST_ROOM_ID, event);
|
||||
|
||||
// Wait for the request to be received
|
||||
await emitPromise(aliceClient, CryptoEvent.VerificationRequestReceived);
|
||||
|
||||
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
|
||||
|
||||
// When I cancel it
|
||||
fetchMock.put("express:/_matrix/client/v3/rooms/:roomId/send/m.key.verification.cancel/:id", {
|
||||
event_id: event.event_id,
|
||||
});
|
||||
await request!.cancel();
|
||||
expect(request!.phase).toEqual(VerificationPhase.Cancelled);
|
||||
|
||||
// Then it is no longer found
|
||||
expect(
|
||||
aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz"),
|
||||
).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("Plaintext verification request from Bob to Alice", async () => {
|
||||
// Add verification request from Bob to Alice in the DM between them
|
||||
returnRoomMessageFromSync(TEST_ROOM_ID, createVerificationRequestEvent());
|
||||
@@ -1115,7 +1177,7 @@ describe("verification", () => {
|
||||
returnToDeviceMessageFromSync(toDeviceEvent);
|
||||
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// Wait for the request to be decrypted
|
||||
const request1 = await requestEventPromise;
|
||||
@@ -1152,7 +1214,7 @@ describe("verification", () => {
|
||||
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
|
||||
// Advance time by 5mins, the verification request should be ignored after that
|
||||
jest.advanceTimersByTime(5 * 60 * 1000);
|
||||
vi.advanceTimersByTime(5 * 60 * 1000);
|
||||
|
||||
// Send Bob the room keys
|
||||
returnToDeviceMessageFromSync(toDeviceEvent);
|
||||
@@ -1218,7 +1280,7 @@ describe("verification", () => {
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
|
||||
await syncPromise(aliceClient);
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the olm device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
@@ -1230,11 +1292,10 @@ describe("verification", () => {
|
||||
testOlmAccount?.free();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
fetchMock.mockReset();
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("Should request cross signing keys after verification", async () => {
|
||||
const requestPromises = mockSecretRequestAndGetPromises();
|
||||
|
||||
@@ -1352,11 +1413,11 @@ describe("verification", () => {
|
||||
*/
|
||||
async function retrieveBackupPrivateKeyWithDelay(): Promise<Uint8Array | null> {
|
||||
// We are lacking a way to signal that the secret has been received, so we wait a bit..
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
vi.useFakeTimers();
|
||||
|
||||
return aliceClient.getCrypto()!.getSessionBackupPrivateKey();
|
||||
}
|
||||
@@ -1389,27 +1450,21 @@ describe("verification", () => {
|
||||
});
|
||||
|
||||
const expectBackupCheck = new Promise((resolve) => {
|
||||
fetchMock.get(
|
||||
"express:/_matrix/client/v3/room_keys/version",
|
||||
(url, request) => {
|
||||
resolve(undefined);
|
||||
if (expectBackup instanceof MatrixError) {
|
||||
return {
|
||||
status: expectBackup.httpStatus,
|
||||
body: expectBackup.data,
|
||||
};
|
||||
}
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/version", (callLog) => {
|
||||
resolve(undefined);
|
||||
if (expectBackup instanceof MatrixError) {
|
||||
return {
|
||||
status: expectBackup.httpStatus,
|
||||
body: expectBackup.data,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectBackup instanceof Error) {
|
||||
return Promise.reject(expectBackup);
|
||||
}
|
||||
if (expectBackup instanceof Error) {
|
||||
return Promise.reject(expectBackup);
|
||||
}
|
||||
|
||||
return expectBackup;
|
||||
},
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
return expectBackup;
|
||||
});
|
||||
});
|
||||
|
||||
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
|
||||
@@ -1476,6 +1531,7 @@ describe("verification", () => {
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "device_under_test",
|
||||
logger: new DebugLogger(debug(`matrix-js-sdk:verification`)),
|
||||
...opts,
|
||||
});
|
||||
await client.initRustCrypto();
|
||||
@@ -1489,7 +1545,7 @@ describe("verification", () => {
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
// The client should now know about the dummy device
|
||||
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||
@@ -1523,8 +1579,8 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
|
||||
return new Promise((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
(callLog): RouteResponse => {
|
||||
resolve(JSON.parse(callLog.options.body as string));
|
||||
return {};
|
||||
},
|
||||
);
|
||||
@@ -1545,29 +1601,25 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
|
||||
const uskRequestResolvers = Promise.withResolvers<string>();
|
||||
const backupKeyRequestResolvers = Promise.withResolvers<string>();
|
||||
|
||||
fetchMock.put(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
|
||||
(url: string, opts: RequestInit): FetchMock.MockResponse => {
|
||||
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
|
||||
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
||||
const content = Object.values(messages)[0] as any;
|
||||
if (content.action == "request") {
|
||||
const name = content.name;
|
||||
const requestId = content.request_id;
|
||||
if (name == "m.cross_signing.user_signing") {
|
||||
uskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.master") {
|
||||
mskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.self_signing") {
|
||||
sskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.megolm_backup.v1") {
|
||||
backupKeyRequestResolvers.resolve(requestId);
|
||||
}
|
||||
fetchMock.put(new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`), (callLog): RouteResponse => {
|
||||
const messages = JSON.parse(callLog.options.body as string).messages[TEST_USER_ID];
|
||||
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
|
||||
const content = Object.values(messages)[0] as any;
|
||||
if (content.action == "request") {
|
||||
const name = content.name;
|
||||
const requestId = content.request_id;
|
||||
if (name == "m.cross_signing.user_signing") {
|
||||
uskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.master") {
|
||||
mskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.cross_signing.self_signing") {
|
||||
sskRequestResolvers.resolve(requestId);
|
||||
} else if (name == "m.megolm_backup.v1") {
|
||||
backupKeyRequestResolvers.resolve(requestId);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const promiseMap = new Map<string, Promise<string>>();
|
||||
promiseMap.set("m.cross_signing.master", mskRequestResolvers.promise);
|
||||
|
||||
@@ -672,7 +672,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(timeline!.getEvents().find((e) => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
|
||||
it("should return null when event is not in the thread that the given timelineSet is representing", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeUndefined(),
|
||||
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeNull(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return undefined when event is within a thread but timelineSet is not", () => {
|
||||
it("should return null when event is within a thread but timelineSet is not", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeUndefined(),
|
||||
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeNull(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
@@ -2044,6 +2044,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(timeline!.getEvents()[1]!.event).toEqual(THREAD_REPLY);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("in stable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
|
||||
@@ -267,6 +267,59 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("invite", function () {
|
||||
it("should send request to /invite", async () => {
|
||||
const roomId = "!roomId:server";
|
||||
const userId = "@user:server";
|
||||
|
||||
httpBackend
|
||||
.when("POST", `/rooms/${encodeURIComponent(roomId)}/invite`)
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({ user_id: userId });
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
const prom = client.invite(roomId, userId);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
it("accepts a stringy reason argument", async () => {
|
||||
const roomId = "!roomId:server";
|
||||
const userId = "@user:server";
|
||||
|
||||
httpBackend
|
||||
.when("POST", `/rooms/${encodeURIComponent(roomId)}/invite`)
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({ user_id: userId, reason: "testreason" });
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
const prom = client.invite(roomId, userId, "testreason");
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
it("accepts an options object with a reason", async () => {
|
||||
const roomId = "!roomId:server";
|
||||
const userId = "@user:server";
|
||||
|
||||
httpBackend
|
||||
.when("POST", `/rooms/${encodeURIComponent(roomId)}/invite`)
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({ user_id: userId, reason: "testreason" });
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
const prom = client.invite(roomId, userId, { reason: "testreason" });
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
});
|
||||
|
||||
describe("knockRoom", function () {
|
||||
const roomId = "!some-room-id:example.org";
|
||||
const reason = "some reason";
|
||||
@@ -294,6 +347,7 @@ describe("MatrixClient", function () {
|
||||
expect((await prom).room_id).toBe(roomId);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should no-op if you've already knocked a room", function () {
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
@@ -327,23 +381,16 @@ describe("MatrixClient", function () {
|
||||
[
|
||||
403,
|
||||
{ errcode: "M_FORBIDDEN", error: "You don't have permission to knock" },
|
||||
"[M_FORBIDDEN: MatrixError: [403] You don't have permission to knock]",
|
||||
],
|
||||
[
|
||||
500,
|
||||
{ errcode: "INTERNAL_SERVER_ERROR" },
|
||||
"[INTERNAL_SERVER_ERROR: MatrixError: [500] Unknown message]",
|
||||
"MatrixError: [403] You don't have permission to knock",
|
||||
],
|
||||
[500, { errcode: "INTERNAL_SERVER_ERROR" }, "MatrixError: [500] Unknown message"],
|
||||
];
|
||||
|
||||
it.each(testCases)("should handle %s error", async (code, { errcode, error }, snapshot) => {
|
||||
httpBackend.when("POST", "/knock/" + encodeURIComponent(roomId)).respond(code, { errcode, error });
|
||||
|
||||
const prom = client.knockRoom(roomId);
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
expect(prom).rejects.toMatchInlineSnapshot(snapshot),
|
||||
]);
|
||||
await Promise.all([httpBackend.flushAllExpected(), expect(prom).rejects.toThrow(snapshot)]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1145,7 +1192,7 @@ describe("MatrixClient", function () {
|
||||
describe("logout", () => {
|
||||
it("should abort pending requests when called with stopClient=true", async () => {
|
||||
httpBackend.when("POST", "/logout").respond(200, {});
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
client.http.request(Method.Get, "/test").catch(fn);
|
||||
client.logout(true);
|
||||
await httpBackend.flush(undefined);
|
||||
@@ -1273,7 +1320,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should always fetch capabilities and then cache", async () => {
|
||||
@@ -1344,6 +1391,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("publicRooms", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should use GET request if no server or filter is specified", () => {
|
||||
httpBackend.when("GET", "/publicRooms").respond(200, {});
|
||||
client.publicRooms({});
|
||||
@@ -1532,7 +1580,7 @@ describe("MatrixClient", function () {
|
||||
|
||||
describe("setSyncPresence", () => {
|
||||
it("should pass calls through to the underlying sync api", () => {
|
||||
const setPresence = jest.fn();
|
||||
const setPresence = vi.fn();
|
||||
// @ts-ignore
|
||||
client.syncApi = { setPresence };
|
||||
client.setSyncPresence(SetPresence.Unavailable);
|
||||
@@ -1541,6 +1589,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("sendTyping", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should bail early for guests", async () => {
|
||||
client.setGuest(true);
|
||||
await client.sendTyping("!room:server", true, 100);
|
||||
@@ -1798,6 +1847,7 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("setRoomMutePushRule", () => {
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should set room push rule to muted", async () => {
|
||||
const roomId = "!roomId:server";
|
||||
const client = new MatrixClient({
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("MatrixClient opts", function () {
|
||||
|
||||
await expect(
|
||||
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
|
||||
).rejects.toThrow("MatrixError: [500] Unknown message");
|
||||
).rejects.toThrow("MatrixError: [500] Ruh roh");
|
||||
});
|
||||
|
||||
it("shouldn't queue events", async () => {
|
||||
|
||||
@@ -720,7 +720,7 @@ describe("MatrixClient room timelines", function () {
|
||||
} else {
|
||||
reject(new Error("TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire."));
|
||||
}
|
||||
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
|
||||
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Vitest? */);
|
||||
|
||||
room.on(RoomEvent.TimelineReset, async () => {
|
||||
try {
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { type MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
|
||||
|
||||
@@ -83,8 +83,7 @@ describe("MatrixClient syncing errors", () => {
|
||||
});
|
||||
|
||||
it("should retry, until errors are solved.", async () => {
|
||||
jest.useFakeTimers();
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
vi.useFakeTimers();
|
||||
fetchMock
|
||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
|
||||
@@ -105,19 +104,18 @@ describe("MatrixClient syncing errors", () => {
|
||||
|
||||
await client!.startClient();
|
||||
expect(await syncEvents[0].promise).toBe(SyncState.Error);
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[1].promise).toBe(SyncState.Error);
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
|
||||
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
vi.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
|
||||
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
|
||||
});
|
||||
|
||||
it("should stop sync keep alive when client is stopped.", async () => {
|
||||
jest.useFakeTimers();
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
vi.useFakeTimers();
|
||||
fetchMock
|
||||
.get("end:capabilities", {})
|
||||
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
|
||||
@@ -146,9 +144,9 @@ describe("MatrixClient syncing errors", () => {
|
||||
|
||||
const syntState = await firstSyncEvent.promise;
|
||||
expect(syntState).toBe(SyncState.Error);
|
||||
jest.runAllTimers(); // this will skip forward to trigger the keepAlive
|
||||
vi.runAllTimers(); // this will skip forward to trigger the keepAlive
|
||||
|
||||
jest.useRealTimers(); // we need real timer for the setTimout below to work
|
||||
vi.useRealTimers(); // we need real timer for the setTimout below to work
|
||||
|
||||
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ describe("MatrixClient syncing", () => {
|
||||
presence: {},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should /sync after /pushrules and /filter.", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
@@ -501,7 +502,7 @@ describe("MatrixClient syncing", () => {
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
|
||||
return httpBackend!.flushAllExpected();
|
||||
@@ -994,7 +995,7 @@ describe("MatrixClient syncing", () => {
|
||||
roomVersion: "org.matrix.msc2716v3",
|
||||
},
|
||||
].forEach((testMeta) => {
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
// eslint-disable-next-line @vitest/valid-title
|
||||
describe(testMeta.label, () => {
|
||||
const roomCreateEvent = utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
@@ -1835,7 +1836,7 @@ describe("MatrixClient syncing", () => {
|
||||
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
|
||||
|
||||
const room = client!.getRoom(roomOne);
|
||||
room!.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
|
||||
room!.hasEncryptionStateEvent = vi.fn().mockReturnValue(true);
|
||||
|
||||
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
|
||||
|
||||
@@ -2519,7 +2520,7 @@ describe("MatrixClient syncing", () => {
|
||||
const eventB2 = new MatrixEvent({ type: "b", content: { body: "2" } });
|
||||
|
||||
client!.store.storeAccountDataEvents([eventA1, eventB1]);
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
client!.on(ClientEvent.AccountData, fn);
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("Notification count fixing", () => {
|
||||
|
||||
client!.startClient({ threadSupport: true });
|
||||
const room = new Room(roomId, client!, selfUserId);
|
||||
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: roomId,
|
||||
@@ -77,7 +77,7 @@ describe("Notification count fixing", () => {
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(event, "getPushActions").mockReturnValue({
|
||||
vi.spyOn(event, "getPushActions").mockReturnValue({
|
||||
notify: true,
|
||||
tweaks: {},
|
||||
});
|
||||
@@ -123,7 +123,7 @@ describe("MatrixClient syncing", () => {
|
||||
]);
|
||||
|
||||
const room = new Room(roomId, client!, selfUserId);
|
||||
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
vi.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
|
||||
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||
const threadReply = thread.events.at(-1)!;
|
||||
@@ -143,7 +143,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
|
||||
let lastEvent: MatrixEvent | null = null;
|
||||
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
|
||||
vi.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
|
||||
lastEvent = event as MatrixEvent;
|
||||
return { event_id: reactionEventId };
|
||||
});
|
||||
@@ -195,7 +195,7 @@ describe("MatrixClient syncing", () => {
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.store.getSavedSyncToken = vi.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { mocked } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import {
|
||||
MSC4108FailureReason,
|
||||
@@ -40,7 +39,7 @@ import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
||||
const baseUrl = "https://example.com";
|
||||
const crypto = {
|
||||
exportSecretsForQrLogin: jest.fn(),
|
||||
exportSecretsForQrLogin: vi.fn(),
|
||||
};
|
||||
const client = {
|
||||
doesServerSupportUnstableFeature(feature: string) {
|
||||
@@ -54,9 +53,9 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
|
||||
},
|
||||
baseUrl,
|
||||
getDomain: () => "example.com",
|
||||
getDevice: jest.fn(),
|
||||
getCrypto: jest.fn(() => crypto),
|
||||
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
||||
getDevice: vi.fn(),
|
||||
getCrypto: vi.fn(() => crypto),
|
||||
getAuthMetadata: vi.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
||||
} as unknown as MatrixClient;
|
||||
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
||||
baseUrl: client.baseUrl,
|
||||
@@ -77,10 +76,6 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
const url = "https://fallbackserver/rz/123";
|
||||
const deviceId = "DEADB33F";
|
||||
const verificationUri = "https://example.com/verify";
|
||||
@@ -115,10 +110,10 @@ describe("MSC4108SignInWithQR", () => {
|
||||
let opponentData = Promise.withResolvers<string>();
|
||||
|
||||
const ourMockSession = {
|
||||
send: jest.fn(async (newData) => {
|
||||
send: vi.fn(async (newData) => {
|
||||
ourData.resolve(newData);
|
||||
}),
|
||||
receive: jest.fn(() => {
|
||||
receive: vi.fn(() => {
|
||||
const prom = opponentData.promise;
|
||||
prom.then(() => {
|
||||
opponentData = Promise.withResolvers();
|
||||
@@ -134,10 +129,10 @@ describe("MSC4108SignInWithQR", () => {
|
||||
},
|
||||
} as unknown as MSC4108RendezvousSession;
|
||||
const opponentMockSession = {
|
||||
send: jest.fn(async (newData) => {
|
||||
send: vi.fn(async (newData) => {
|
||||
opponentData.resolve(newData);
|
||||
}),
|
||||
receive: jest.fn(() => {
|
||||
receive: vi.fn(() => {
|
||||
const prom = ourData.promise;
|
||||
prom.then(() => {
|
||||
ourData = Promise.withResolvers();
|
||||
@@ -151,7 +146,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
|
||||
const ourChannel = new MSC4108SecureChannel(ourMockSession);
|
||||
const qrCodeData = QrCodeData.fromBytes(
|
||||
await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getDomain()!),
|
||||
await ourChannel.generateCode(QrCodeIntent.Reciprocate, client.getDomain()!),
|
||||
);
|
||||
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
|
||||
|
||||
@@ -171,7 +166,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
it("should be able to connect with opponent and share verificationUri", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).resolves.toEqual({
|
||||
@@ -194,7 +189,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
it("should abort if device already exists", async () => {
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
|
||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourLogin.deviceAuthorizationGrant()).rejects.toThrow("Specified device ID already exists"),
|
||||
@@ -244,12 +239,12 @@ describe("MSC4108SignInWithQR", () => {
|
||||
// @ts-ignore
|
||||
await opponentLogin.receive();
|
||||
|
||||
mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
vi.mocked(client.getDevice).mockResolvedValue({} as IMyDevice);
|
||||
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
|
||||
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
|
||||
|
||||
const payload = {
|
||||
secrets: expect.objectContaining(secrets),
|
||||
@@ -261,13 +256,13 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
|
||||
it("should abort if device doesn't come up by timeout", async () => {
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
});
|
||||
jest.spyOn(Date, "now").mockImplementation(() => {
|
||||
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
|
||||
vi.spyOn(Date, "now").mockImplementation(() => {
|
||||
return 12345678 + vi.mocked(setTimeout).mock.calls.length * 1000;
|
||||
});
|
||||
|
||||
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
|
||||
@@ -280,7 +275,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
vi.mocked(client.getDevice).mockRejectedValue(new MatrixError({ errcode: "M_NOT_FOUND" }, 404));
|
||||
|
||||
const ourProm = ourLogin.shareSecrets();
|
||||
await expect(ourProm).rejects.toThrow("New device not found");
|
||||
@@ -297,7 +292,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
await opponentLogin.send({
|
||||
type: PayloadType.Success,
|
||||
});
|
||||
mocked(client.getDevice).mockRejectedValue(
|
||||
vi.mocked(client.getDevice).mockRejectedValue(
|
||||
new MatrixError({ errcode: "M_UNKNOWN", error: "The message" }, 500),
|
||||
);
|
||||
|
||||
@@ -314,7 +309,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
|
||||
it("should not send secrets if user cancels", async () => {
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
@@ -334,7 +329,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
await opponentLogin.receive();
|
||||
|
||||
const deviceResolvers = Promise.withResolvers<IMyDevice>();
|
||||
mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
|
||||
vi.mocked(client.getDevice).mockReturnValue(deviceResolvers.promise);
|
||||
|
||||
ourLogin.cancel(MSC4108FailureReason.UserCancelled).catch(() => {});
|
||||
deviceResolvers.resolve({} as IMyDevice);
|
||||
@@ -342,7 +337,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
const secrets = {
|
||||
cross_signing: { master_key: "mk", user_signing_key: "usk", self_signing_key: "ssk" },
|
||||
};
|
||||
client.getCrypto()!.exportSecretsBundle = jest.fn().mockResolvedValue(secrets);
|
||||
client.getCrypto()!.exportSecretsBundle = vi.fn().mockResolvedValue(secrets);
|
||||
|
||||
await Promise.all([
|
||||
expect(ourProm).rejects.toThrow("User cancelled"),
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
type Extension,
|
||||
} from "../../src/sliding-sync";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { type IRoomEvent, type IStateEvent } from "../../src";
|
||||
import { type IContent, type IRoomEvent, type IStateEvent } from "../../src";
|
||||
import {
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
@@ -68,17 +68,17 @@ describe("SlidingSyncSdk", () => {
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
|
||||
s.getListParams = jest.fn();
|
||||
s.getListData = jest.fn();
|
||||
s.getRoomSubscriptions = jest.fn();
|
||||
s.modifyRoomSubscriptionInfo = jest.fn();
|
||||
s.modifyRoomSubscriptions = jest.fn();
|
||||
s.registerExtension = jest.fn();
|
||||
s.setList = jest.fn();
|
||||
s.setListRanges = jest.fn();
|
||||
s.start = jest.fn();
|
||||
s.stop = jest.fn();
|
||||
s.resend = jest.fn();
|
||||
s.getListParams = vi.fn();
|
||||
s.getListData = vi.fn();
|
||||
s.getRoomSubscriptions = vi.fn();
|
||||
s.modifyRoomSubscriptionInfo = vi.fn();
|
||||
s.modifyRoomSubscriptions = vi.fn();
|
||||
s.registerExtension = vi.fn();
|
||||
s.setList = vi.fn();
|
||||
s.setListRanges = vi.fn();
|
||||
s.start = vi.fn();
|
||||
s.stop = vi.fn();
|
||||
s.resend = vi.fn();
|
||||
return s;
|
||||
};
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(m.getType()).toEqual(want[i].type);
|
||||
expect(m.getSender()).toEqual(want[i].sender);
|
||||
expect(m.getId()).toEqual(want[i].event_id);
|
||||
expect(m.getContent()).toEqual(want[i].content);
|
||||
expect(m.getContent<IContent>()).toEqual(want[i].content);
|
||||
expect(m.getTs()).toEqual(want[i].origin_server_ts);
|
||||
if (want[i].unsigned) {
|
||||
expect(m.getUnsigned()).toEqual(want[i].unsigned);
|
||||
@@ -126,7 +126,7 @@ describe("SlidingSyncSdk", () => {
|
||||
// assign client/httpBackend globals
|
||||
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
|
||||
testOpts = testOpts || {};
|
||||
const syncOpts: SyncApiOptions = {};
|
||||
const syncOpts: SyncApiOptions = { logger };
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
@@ -150,7 +150,7 @@ describe("SlidingSyncSdk", () => {
|
||||
// find an extension on a SlidingSyncSdk instance
|
||||
const findExtension = (name: string): Extension<any, any> => {
|
||||
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
||||
const mockFn = vi.mocked(mockSlidingSync!.registerExtension);
|
||||
// find the extension
|
||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
|
||||
@@ -658,7 +658,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update device lists", () => {
|
||||
syncCryptoCallback!.processDeviceLists = jest.fn();
|
||||
syncCryptoCallback!.processDeviceLists = vi.fn();
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
changed: ["@alice:localhost"],
|
||||
@@ -672,7 +672,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update OTK counts and unused fallback keys", () => {
|
||||
syncCryptoCallback!.processKeyCounts = jest.fn();
|
||||
syncCryptoCallback!.processKeyCounts = vi.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
@@ -722,7 +722,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeTruthy();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
expect(globalData.getContent<IContent>()).toEqual(globalContent);
|
||||
});
|
||||
|
||||
it("processes rooms account data", async () => {
|
||||
@@ -757,7 +757,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(room).toBeTruthy();
|
||||
const event = room.getAccountData(roomType)!;
|
||||
expect(event).toBeTruthy();
|
||||
expect(event.getContent()).toEqual(roomContent);
|
||||
expect(event.getContent<IContent>()).toEqual(roomContent);
|
||||
});
|
||||
|
||||
it("doesn't crash for unknown room account data", async () => {
|
||||
@@ -847,6 +847,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("can handle missing fields", async () => {
|
||||
ext.onResponse({
|
||||
next_batch: "23456",
|
||||
@@ -861,7 +862,7 @@ describe("SlidingSyncSdk", () => {
|
||||
};
|
||||
let called = false;
|
||||
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
expect(ev.getContent()).toEqual(toDeviceContent);
|
||||
expect(ev.getContent<IContent>()).toEqual(toDeviceContent);
|
||||
expect(ev.getType()).toEqual(toDeviceType);
|
||||
called = true;
|
||||
});
|
||||
@@ -1095,6 +1096,7 @@ describe("SlidingSyncSdk", () => {
|
||||
expect(receipt?.data.thread_id).toBeFalsy();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("gracefully handles missing rooms when receiving receipts", async () => {
|
||||
const roomId = "!room:id";
|
||||
const alice = "@alice:alice";
|
||||
|
||||
@@ -82,6 +82,7 @@ describe("SlidingSync", () => {
|
||||
await p;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should stop the sync loop upon calling stop()", () => {
|
||||
slidingSync.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Vector creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../src/logger";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
globalThis.Olm = require("@matrix-org/olm");
|
||||
logger.log("loaded libolm");
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available", e);
|
||||
}
|
||||
+13
-4
@@ -14,13 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
jest.mock("../src/http-api/utils", () => ({
|
||||
...jest.requireActual("../src/http-api/utils"),
|
||||
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
|
||||
|
||||
vi.mock("../src/http-api/utils", async () => ({
|
||||
...(await vi.importActual("../src/http-api/utils")),
|
||||
// We mock timeoutSignal otherwise it causes tests to leave timers running
|
||||
timeoutSignal: () => new AbortController().signal,
|
||||
}));
|
||||
|
||||
// Dont make test fail too soon due to timeouts while debugging.
|
||||
manageFetchMockGlobally();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.hardReset();
|
||||
fetchMock.mockGlobal();
|
||||
});
|
||||
|
||||
// Don't make test fail too soon due to timeouts while debugging.
|
||||
if (process.env.VSCODE_INSPECTOR_OPTIONS) {
|
||||
jest.setTimeout(60 * 1000 * 5); // 5 minutes
|
||||
vi.setConfig({ testTimeout: 60 * 1000 * 5 }); // 5 minutes
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { type ISyncResponder } from "./SyncResponder";
|
||||
|
||||
@@ -36,65 +36,80 @@ export class AccountDataAccumulator {
|
||||
|
||||
public constructor(private syncResponder: ISyncResponder) {}
|
||||
|
||||
private accountDataResolvers = new Map<string, PromiseWithResolvers<any>>();
|
||||
private setInterceptRunning = false;
|
||||
|
||||
/**
|
||||
* Intercept requests to set a particular type of account data.
|
||||
* Intercept setting of account data.
|
||||
*
|
||||
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
|
||||
* resolved.
|
||||
*
|
||||
* @param accountDataType - type of account data to be intercepted
|
||||
* @param opts - options to pass to fetchMock
|
||||
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||
*/
|
||||
public interceptSetAccountData(
|
||||
accountDataType: string,
|
||||
opts?: Parameters<(typeof fetchMock)["put"]>[2],
|
||||
): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
// Called when the cross signing key is uploaded
|
||||
fetchMock.put(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/${accountDataType}`,
|
||||
(url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const type = url.split("/").pop();
|
||||
// update account data for sync response
|
||||
this.accountDataEvents.set(type!, content);
|
||||
resolve(content);
|
||||
public interceptSetAccountData(): void {
|
||||
if (this.setInterceptRunning) return;
|
||||
this.setInterceptRunning = true;
|
||||
|
||||
// return a sync response
|
||||
this.sendSyncResponseWithUpdatedAccountData();
|
||||
return {};
|
||||
},
|
||||
opts,
|
||||
);
|
||||
fetchMock.put(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
|
||||
const content = JSON.parse(callLog.options.body as string);
|
||||
const type = callLog.url.split("/").pop();
|
||||
// update account data for sync response
|
||||
this.accountDataEvents.set(type!, content);
|
||||
|
||||
this.accountDataResolvers.get(type!)?.resolve(content);
|
||||
if (!this.accountDataResolvers.delete(type!)) {
|
||||
// Check for a wildcard matcher
|
||||
for (const [key, resolver] of this.accountDataResolvers) {
|
||||
if (key.endsWith("*") && type?.startsWith(key.slice(0, -1))) {
|
||||
resolver.resolve(content);
|
||||
this.accountDataResolvers.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return a sync response
|
||||
this.sendSyncResponseWithUpdatedAccountData();
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a particular type of account data.
|
||||
*
|
||||
* Once it is set, its data is stored (for future return by `interceptGetAccountData` etc) and the resolved promise is
|
||||
* resolved.
|
||||
*
|
||||
* @returns a Promise which will resolve (with the content of the account data) once it is set.
|
||||
*/
|
||||
public waitForAccountData(type: string): Promise<any> {
|
||||
const resolvers = Promise.withResolvers<any>();
|
||||
this.accountDataResolvers.set(type, resolvers);
|
||||
this.interceptSetAccountData();
|
||||
return resolvers.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept all requests to get account data
|
||||
*/
|
||||
public interceptGetAccountData(): void {
|
||||
fetchMock.get(
|
||||
`express:/_matrix/client/v3/user/:userId/account_data/:type`,
|
||||
(url) => {
|
||||
const type = url.split("/").pop();
|
||||
const existing = this.accountDataEvents.get(type!);
|
||||
if (existing) {
|
||||
// return it
|
||||
return {
|
||||
status: 200,
|
||||
body: existing,
|
||||
};
|
||||
} else {
|
||||
// 404
|
||||
return {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
};
|
||||
}
|
||||
},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.get(`express:/_matrix/client/v3/user/:userId/account_data/:type`, (callLog) => {
|
||||
const type = callLog.url.split("/").pop();
|
||||
const existing = this.accountDataEvents.get(type!);
|
||||
if (existing) {
|
||||
// return it
|
||||
return {
|
||||
status: 200,
|
||||
body: existing,
|
||||
};
|
||||
} else {
|
||||
// 404
|
||||
return {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import debugFunc from "debug";
|
||||
import { type Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import debugFunc, { type Debugger } from "debug";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||
import type { CrossSigningKeys, ISignedKey, KeySignatures } from "../../src";
|
||||
import type { CrossSigningKeyInfo } from "../../src/crypto-api";
|
||||
|
||||
/** Interface implemented by classes that intercept `/keys/upload` requests from test clients to catch the uploaded keys
|
||||
*
|
||||
@@ -55,28 +56,53 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
private readonly debug: Debugger;
|
||||
|
||||
private deviceKeys: IDeviceKeys | null = null;
|
||||
private crossSigningKeys: CrossSigningKeys | null = null;
|
||||
private oneTimeKeys: Record<string, IOneTimeKey> = {};
|
||||
private readonly oneTimeKeysPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* Construct a new E2EKeyReceiver.
|
||||
*
|
||||
* It will immediately register an intercept of `/keys/uploads` requests for the given homeserverUrl.
|
||||
* Only /upload requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* It will immediately register an intercept of [`/keys/upload`][1], [`/keys/signatures/upload`][2] and
|
||||
* [`/keys/device_signing/upload`][3] requests for the given homeserverUrl.
|
||||
* Only requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have the keys collected separately.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
* [1]: https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3keysupload
|
||||
* [2]: https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3keyssignaturesupload
|
||||
* [3]: https://spec.matrix.org/v1.14/client-server-api/#post_matrixclientv3keysdevice_signingupload
|
||||
*
|
||||
* @param homeserverUrl - The Homeserver Url of the client under test.
|
||||
* @param routeNamePrefix - An optional prefix to add to the fetchmock route names. Required if there is more than
|
||||
* one E2EKeyReceiver instance active.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
public constructor(homeserverUrl: string, routeNamePrefix: string = "") {
|
||||
this.debug = debugFunc(`e2e-key-receiver:[${homeserverUrl}]`);
|
||||
|
||||
// set up a listener for /keys/upload.
|
||||
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
|
||||
const listener = (url: string, options: RequestInit) =>
|
||||
this.onKeyUploadRequest(resolveOneTimeKeys, options);
|
||||
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(),
|
||||
(callLog) => this.onKeyUploadRequest(resolveOneTimeKeys, callLog.options),
|
||||
{ name: routeNamePrefix + "keys-upload" },
|
||||
);
|
||||
});
|
||||
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/signatures/upload", homeserverUrl).toString(),
|
||||
(callLog) => this.onSignaturesUploadRequest(callLog.options),
|
||||
{
|
||||
name: routeNamePrefix + "upload-sigs",
|
||||
},
|
||||
);
|
||||
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/device_signing/upload", homeserverUrl).toString(),
|
||||
(callLog) => this.onSigningKeyUploadRequest(callLog.options),
|
||||
{
|
||||
name: routeNamePrefix + "upload-cross-signing-keys",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async onKeyUploadRequest(onOnTimeKeysUploaded: () => void, options: RequestInit): Promise<object> {
|
||||
@@ -87,8 +113,10 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
if (this.deviceKeys) {
|
||||
throw new Error("Application attempted to upload E2E device keys multiple times");
|
||||
}
|
||||
this.debug(`received device keys`);
|
||||
this.deviceKeys = content.device_keys;
|
||||
this.debug(
|
||||
`received device keys for user ID ${this.deviceKeys!.user_id}, device ID ${this.deviceKeys!.device_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (content.one_time_keys && Object.keys(content.one_time_keys).length > 0) {
|
||||
@@ -113,6 +141,47 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
};
|
||||
}
|
||||
|
||||
private async onSignaturesUploadRequest(request: RequestInit): Promise<object> {
|
||||
const content = JSON.parse(request.body as string) as KeySignatures;
|
||||
for (const [userId, userKeys] of Object.entries(content)) {
|
||||
for (const [deviceId, signedKey] of Object.entries(userKeys)) {
|
||||
this.onDeviceSignatureUpload(userId, deviceId, signedKey);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private onDeviceSignatureUpload(userId: string, deviceId: string, signedKey: CrossSigningKeyInfo | ISignedKey) {
|
||||
if (!this.deviceKeys || userId != this.deviceKeys.user_id || deviceId != this.deviceKeys.device_id) {
|
||||
this.debug(
|
||||
`Ignoring device key signature upload for unknown device user ID ${userId}, device ID ${deviceId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug(`received device key signature for user ID ${userId}, device ID ${deviceId}`);
|
||||
this.deviceKeys.signatures ??= {};
|
||||
for (const [signingUser, signatures] of Object.entries(signedKey.signatures!)) {
|
||||
this.deviceKeys.signatures[signingUser] = Object.assign(
|
||||
this.deviceKeys.signatures[signingUser] ?? {},
|
||||
signatures,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async onSigningKeyUploadRequest(request: RequestInit): Promise<object> {
|
||||
const content = JSON.parse(request.body as string);
|
||||
if (this.crossSigningKeys) {
|
||||
throw new Error("Application attempted to upload E2E cross-signing keys multiple times");
|
||||
}
|
||||
this.debug(`received cross-signing keys`);
|
||||
// Remove UIA data
|
||||
delete content["auth"];
|
||||
this.crossSigningKeys = content;
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Get the uploaded Ed25519 key
|
||||
*
|
||||
* If device keys have not yet been uploaded, throws an error
|
||||
@@ -150,6 +219,13 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
return this.deviceKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* If cross-signing keys have been uploaded, return them. Else return null.
|
||||
*/
|
||||
public getUploadedCrossSigningKeys(): CrossSigningKeys | null {
|
||||
return this.crossSigningKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
@@ -161,4 +237,18 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
await this.oneTimeKeysPromise;
|
||||
return this.oneTimeKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* If no one-time keys have yet been uploaded, return `null`.
|
||||
* Otherwise, pop a key from the uploaded list.
|
||||
*/
|
||||
public getOneTimeKey(): [string, IOneTimeKey] | null {
|
||||
const keys = Object.entries(this.oneTimeKeys);
|
||||
if (keys.length == 0) {
|
||||
return null;
|
||||
}
|
||||
const [otkId, otk] = keys[0];
|
||||
delete this.oneTimeKeys[otkId];
|
||||
return [otkId, otk];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { MapWithDefault } from "../../src/utils";
|
||||
import { type IDownloadKeyResult } from "../../src";
|
||||
import { type IDownloadKeyResult, type SigningKeys } from "../../src";
|
||||
import { type IDeviceKeys } from "../../src/@types/crypto";
|
||||
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
|
||||
@@ -42,26 +42,23 @@ export class E2EKeyResponder {
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
// set up a listener for /keys/query.
|
||||
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), (callLog) =>
|
||||
this.onKeyQueryRequest(callLog.options),
|
||||
);
|
||||
}
|
||||
|
||||
private onKeyQueryRequest(options: RequestInit) {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const usersToReturn = Object.keys(content["device_keys"]);
|
||||
const response = {
|
||||
device_keys: {} as { [userId: string]: any },
|
||||
master_keys: {} as { [userId: string]: any },
|
||||
self_signing_keys: {} as { [userId: string]: any },
|
||||
user_signing_keys: {} as { [userId: string]: any },
|
||||
failures: {} as { [serverName: string]: any },
|
||||
};
|
||||
device_keys: {},
|
||||
master_keys: {},
|
||||
self_signing_keys: {},
|
||||
user_signing_keys: {},
|
||||
failures: {},
|
||||
} as IDownloadKeyResult;
|
||||
for (const user of usersToReturn) {
|
||||
const userKeys = this.deviceKeysByUserByDevice.get(user);
|
||||
if (userKeys !== undefined) {
|
||||
response.device_keys[user] = Object.fromEntries(userKeys.entries());
|
||||
}
|
||||
|
||||
// First see if we have an E2EKeyReceiver for this user, and if so, return any keys that have been uploaded
|
||||
const e2eKeyReceiver = this.e2eKeyReceiversByUser.get(user);
|
||||
if (e2eKeyReceiver !== undefined) {
|
||||
const deviceKeys = e2eKeyReceiver.getUploadedDeviceKeys();
|
||||
@@ -69,16 +66,27 @@ export class E2EKeyResponder {
|
||||
response.device_keys[user] ??= {};
|
||||
response.device_keys[user][deviceKeys.device_id] = deviceKeys;
|
||||
}
|
||||
const crossSigningKeys = e2eKeyReceiver.getUploadedCrossSigningKeys();
|
||||
if (crossSigningKeys !== null) {
|
||||
response.master_keys![user] = crossSigningKeys["master_key"];
|
||||
response.self_signing_keys![user] = crossSigningKeys["self_signing_key"] as SigningKeys;
|
||||
}
|
||||
}
|
||||
|
||||
// Mix in any keys that have been added explicitly to this E2EKeyResponder.
|
||||
const userKeys = this.deviceKeysByUserByDevice.get(user);
|
||||
if (userKeys !== undefined) {
|
||||
response.device_keys[user] ??= {};
|
||||
Object.assign(response.device_keys[user], Object.fromEntries(userKeys.entries()));
|
||||
}
|
||||
if (this.masterKeysByUser.hasOwnProperty(user)) {
|
||||
response.master_keys[user] = this.masterKeysByUser[user];
|
||||
response.master_keys![user] = this.masterKeysByUser[user];
|
||||
}
|
||||
if (this.selfSigningKeysByUser.hasOwnProperty(user)) {
|
||||
response.self_signing_keys[user] = this.selfSigningKeysByUser[user];
|
||||
response.self_signing_keys![user] = this.selfSigningKeysByUser[user];
|
||||
}
|
||||
if (this.userSigningKeysByUser.hasOwnProperty(user)) {
|
||||
response.user_signing_keys[user] = this.userSigningKeysByUser[user];
|
||||
response.user_signing_keys![user] = this.userSigningKeysByUser[user];
|
||||
}
|
||||
}
|
||||
return response;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright 2025 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 fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { MapWithDefault } from "../../src/utils";
|
||||
import { type E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
import { type IClaimKeysRequest } from "../../src";
|
||||
|
||||
/**
|
||||
* An object which intercepts `/keys/claim` fetches via fetch-mock.
|
||||
*/
|
||||
export class E2EOTKClaimResponder {
|
||||
private e2eKeyReceiversByUserByDevice = new MapWithDefault<string, Map<string, E2EKeyReceiver>>(() => new Map());
|
||||
|
||||
/**
|
||||
* Construct a new E2EOTKClaimResponder.
|
||||
*
|
||||
* It will immediately register an intercept of `/keys/claim` requests for the given homeserverUrl.
|
||||
* Only /claim requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have the keys collected separately.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), (callLog) =>
|
||||
this.onKeyClaimRequest(callLog.options),
|
||||
);
|
||||
}
|
||||
|
||||
private onKeyClaimRequest(options: RequestInit) {
|
||||
const content = JSON.parse(options.body as string) as IClaimKeysRequest;
|
||||
const response = {
|
||||
one_time_keys: {} as { [userId: string]: any },
|
||||
};
|
||||
for (const [userId, devices] of Object.entries(content["one_time_keys"])) {
|
||||
for (const deviceId of Object.keys(devices)) {
|
||||
const e2eKeyReceiver = this.e2eKeyReceiversByUserByDevice.get(userId)?.get(deviceId);
|
||||
const otk = e2eKeyReceiver?.getOneTimeKey();
|
||||
if (otk) {
|
||||
const [keyId, key] = otk;
|
||||
response.one_time_keys[userId] ??= {};
|
||||
response.one_time_keys[userId][deviceId] = {
|
||||
[keyId]: key,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an E2EKeyReceiver to poll for uploaded keys
|
||||
*
|
||||
* When the `/keys/claim` request is received, a OTK will be removed from the `E2EKeyReceiver` and
|
||||
* added to the response.
|
||||
*/
|
||||
public addKeyReceiver(userId: string, deviceId: string, e2eKeyReceiver: E2EKeyReceiver) {
|
||||
this.e2eKeyReceiversByUserByDevice.getOrCreate(userId).set(deviceId, e2eKeyReceiver);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,8 @@ limitations under the License.
|
||||
|
||||
import debugFunc from "debug";
|
||||
import { type Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import type FetchMock from "fetch-mock";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
import { type RouteResponse } from "fetch-mock";
|
||||
|
||||
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
||||
*
|
||||
@@ -76,12 +75,12 @@ export class SyncResponder implements ISyncResponder {
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (callLog) =>
|
||||
this.onSyncRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
|
||||
private async onSyncRequest(): Promise<RouteResponse> {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE: {
|
||||
this.debug("Got /sync request: waiting for response to be ready");
|
||||
|
||||
+23
-17
@@ -14,12 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MethodLikeKeys, mocked, type MockedObject } from "jest-mock";
|
||||
import { type MockedObject } from "vitest";
|
||||
|
||||
import { type ClientEventHandlerMap, type EmittedEvents, type MatrixClient } from "../../src/client";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { User } from "../../src/models/user";
|
||||
|
||||
// Cribbed from https://github.com/jestjs/jest/blob/94830794dc5dfca1b49bc435b7b031b27838a798/packages/jest-mock/src/index.ts
|
||||
type FunctionLike = (...args: any) => any;
|
||||
type MethodLikeKeys<T> = keyof {
|
||||
[K in keyof T as Required<T>[K] extends FunctionLike ? K : never]: T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock client with real event emitter
|
||||
* useful for testing code that listens
|
||||
@@ -34,19 +40,19 @@ export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents,
|
||||
|
||||
/**
|
||||
* - make a mock client
|
||||
* - cast the type to mocked(MatrixClient)
|
||||
* - cast the type to vi.mocked(MatrixClient)
|
||||
* - spy on MatrixClientPeg.get to return the mock
|
||||
* eg
|
||||
* ```
|
||||
* const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||
getUserId: vi.fn().mockReturnValue(aliceId),
|
||||
});
|
||||
* ```
|
||||
*/
|
||||
export const getMockClientWithEventEmitter = (
|
||||
mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>>,
|
||||
): MockedObject<MatrixClient> => {
|
||||
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||
const mock = vi.mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||
return mock;
|
||||
};
|
||||
|
||||
@@ -59,14 +65,14 @@ export const getMockClientWithEventEmitter = (
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
getUserId: vi.fn().mockReturnValue(userId),
|
||||
getSafeUserId: vi.fn().mockReturnValue(userId),
|
||||
getUser: vi.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: vi.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: vi.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
credentials: { userId },
|
||||
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: jest.fn(),
|
||||
getThreePids: vi.fn().mockResolvedValue({ threepids: [] }),
|
||||
getAccessToken: vi.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -78,16 +84,16 @@ export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
* ```
|
||||
*/
|
||||
export const mockClientMethodsEvents = () => ({
|
||||
decryptEventIfNeeded: jest.fn(),
|
||||
getPushActionsForEvent: jest.fn(),
|
||||
decryptEventIfNeeded: vi.fn(),
|
||||
getPushActionsForEvent: vi.fn(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns basic mocked client methods related to server support
|
||||
*/
|
||||
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
|
||||
getIdentityServerUrl: jest.fn(),
|
||||
getHomeserverUrl: jest.fn(),
|
||||
getCachedCapabilities: jest.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||
getIdentityServerUrl: vi.fn(),
|
||||
getHomeserverUrl: vi.fn(),
|
||||
getCachedCapabilities: vi.fn().mockReturnValue({}),
|
||||
doesServerSupportUnstableFeature: vi.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
||||
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MockInstance } from "vitest";
|
||||
|
||||
/**
|
||||
* Filter emitter.emit mock calls to find relevant events
|
||||
* eg:
|
||||
* ```
|
||||
* const emitSpy = jest.spyOn(state, 'emit');
|
||||
* const emitSpy = vi.spyOn(state, 'emit');
|
||||
* << actions >>
|
||||
* const beaconLivenessEmits = emitCallsByEventType(BeaconEvent.New, emitSpy);
|
||||
* expect(beaconLivenessEmits.length).toBe(1);
|
||||
* ```
|
||||
*/
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: MockInstance<(...args: any[]) => any>) =>
|
||||
spy.mock.calls.filter((args) => args[0] === eventType);
|
||||
|
||||
@@ -14,12 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||
// other async methods which break the event loop, letting scheduled promise
|
||||
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||
// until the thing we expect happens: hopefully this is the least flakey way
|
||||
// and avoids assuming anything about the app's behaviour.
|
||||
// Vitest lacks tickAsync() and a number of other async methods which break the event loop,
|
||||
// letting scheduled promise callbacks run. So we have to do it manually
|
||||
// (this is what sinon does under the hood). We do both in a loop until the thing we expect happens:
|
||||
// hopefully this is the least flakey way and avoids assuming anything about the app's behaviour.
|
||||
const realSetTimeout = setTimeout;
|
||||
export function flushPromises() {
|
||||
return new Promise((r) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { type KeyBackupInfo } from "../../src/crypto-api";
|
||||
|
||||
@@ -25,29 +25,22 @@ import { type KeyBackupInfo } from "../../src/crypto-api";
|
||||
* @param userId - the local user's ID. Defaults to `@alice:localhost`.
|
||||
*/
|
||||
export function mockInitialApiRequests(homeserverUrl: string, userId: string = "@alice:localhost") {
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/versions", homeserverUrl).toString(),
|
||||
{ versions: ["v1.1"] },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.getOnce(
|
||||
new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(),
|
||||
{},
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
|
||||
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.postOnce(
|
||||
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, homeserverUrl).toString(),
|
||||
{ filter_id: "fid" },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
fetchMock.getOnce(
|
||||
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter/fid`, homeserverUrl).toString(),
|
||||
{ filter_id: "fid" },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock the requests needed to set up cross signing
|
||||
* Mock the requests needed to set up cross signing, besides those provided by {@link E2EKeyReceiver}.
|
||||
*
|
||||
* Return 404 error for `GET _matrix/client/v3/user/:userId/account_data/:type` request
|
||||
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
|
||||
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
|
||||
*/
|
||||
export function mockSetupCrossSigningRequests(): void {
|
||||
// have account_data requests return an empty object
|
||||
@@ -55,19 +48,6 @@ export function mockSetupCrossSigningRequests(): void {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
|
||||
});
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// ... and one to upload the cross-signing keys (with UIA)
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,24 +60,30 @@ export function mockSetupCrossSigningRequests(): void {
|
||||
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
|
||||
*/
|
||||
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No current backup version",
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "No current backup version",
|
||||
},
|
||||
},
|
||||
});
|
||||
{ name: "room-keys-version" },
|
||||
);
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
|
||||
backupData.version = backupVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
});
|
||||
fetchMock.post(
|
||||
"path:/_matrix/client/v3/room_keys/version",
|
||||
(callLog) => {
|
||||
const backupData: KeyBackupInfo = JSON.parse((callLog.options.body as string) ?? "{}");
|
||||
backupData.version = backupVersion;
|
||||
backupData.count = 0;
|
||||
backupData.etag = "zer";
|
||||
fetchMock.modifyRoute("room-keys-version", { response: backupData });
|
||||
return {
|
||||
version: backupVersion,
|
||||
};
|
||||
},
|
||||
{ name: "post-room-keys-version" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ def main() -> None:
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
||||
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
|
||||
import type {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import type {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
||||
import type {{ KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions }} from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@@ -246,15 +246,6 @@ export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult>
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
|
||||
json.dumps(set_of_exported_room_keys, indent=4)
|
||||
@@ -278,6 +269,23 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
|
||||
|
||||
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
|
||||
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}`.
|
||||
* Contains the key from {prefix}MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const {prefix}PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {{
|
||||
[{prefix}MEGOLM_SESSION_DATA.session_id]: {prefix}CURVE25519_KEY_BACKUP_DATA
|
||||
}};
|
||||
"""
|
||||
|
||||
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
|
||||
@@ -385,7 +393,7 @@ def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
|
||||
def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tuple[dict, ed25519.Ed25519PrivateKey]:
|
||||
"""
|
||||
Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format
|
||||
that can be imported via importRoomKeys API.
|
||||
that can be imported via importRoomKeys API, or shared via MSC4268 room history sharing.
|
||||
Returns the exported key, the matching privat edKey (needed to encrypt)
|
||||
"""
|
||||
index = 0
|
||||
@@ -409,11 +417,12 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
|
||||
"session_id": encode_base64(
|
||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
),
|
||||
"session_key": encode_base64(exported_key),
|
||||
"session_key": encode_base64(bytes(exported_key)),
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
||||
},
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"m.shared_history": True,
|
||||
}
|
||||
|
||||
return megolm_export, private_key
|
||||
@@ -458,7 +467,7 @@ def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed2
|
||||
"room_id": "!room:id",
|
||||
"sender_key": previous["sender_key"],
|
||||
"session_id": previous["session_id"],
|
||||
"session_key": encode_base64(exported_key),
|
||||
"session_key": encode_base64(bytes(exported_key)),
|
||||
"sender_claimed_keys": previous["sender_claimed_keys"],
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
}
|
||||
@@ -609,7 +618,7 @@ def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519
|
||||
|
||||
message += signature
|
||||
|
||||
cipher_text = encode_base64(message)
|
||||
cipher_text = encode_base64(bytes(message))
|
||||
|
||||
encrypted_payload = {
|
||||
"algorithm" : "m.megolm.v1.aes-sha2",
|
||||
@@ -653,7 +662,7 @@ def export_recovery_key(key_b64: str) -> str:
|
||||
export_bytes += parity_byte.to_bytes(1, 'big')
|
||||
|
||||
# The byte string is encoded using base58
|
||||
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
|
||||
recovery_key = base58.b58encode(bytes(export_bytes)).decode('utf-8')
|
||||
|
||||
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
|
||||
return ' '.join(split)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import { type IDeviceKeys, type IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { type IDownloadKeyResult, type IEvent } from "../../../src";
|
||||
import { type KeyBackupSession, type KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
import type { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import type { IDownloadKeyResult, IEvent } from "../../../src";
|
||||
import type { KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions } from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@@ -118,26 +118,6 @@ export const ONE_TIME_KEYS = {
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
@@ -149,7 +129,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -160,7 +141,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -174,7 +156,8 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
};
|
||||
|
||||
/** A ratcheted version of MEGOLM_SESSION_DATA */
|
||||
@@ -196,7 +179,7 @@ export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//Kzlq2o5CssPsw+88gRehkAsPg9Zp5G9sL9to6giltvTWTbsaQpmvv3HLmBOYSFIxvyZrOT/Ffqu325f0IEsKcyV2BdIkw8Ob9Xt+VWoe4MYEGG6y1T8W125zeFgKWI4Ow76uput64H9zZjIo+Cc+hCTO9Ea4EnosSjizCotevkNck7C/zGgfhBikiohROb6SbaZgxicSsEDZ+f7brnri9yP3iXS3PMDHHpa1+XzG2VOG/Y9OQZpkPq+pbLrCC+NWJeJPslDAK5i+RURwzjnPmaHKCRHTq86CwhFyiCDf61MGwCY3xjrmBJg44BCdxWqCx0YJvwsvVqqnl4vTieUfrwThNPsQ81aVkDHvlmrgrTt8icDa8jTJhu34jem+pbRSEM5aJikV4B+zYiLz+dH/v6UpYA2eG8ReOvwpPXp6CAcIlplRPpWbMBeLFVcPkT4KAXTp9exFpB4on4pf8OsaDomlt4qAA0rhAZmhPWPKcU/A0Tz4gyMu54OivVtw1SPj+5Iq+YDQ8jB6Po3ApzMf6fwF9x/FjevbboFB05X2Jr0NrbFqXMOUwXHMgDAGiIWX8+gkmmbaiNWqg2etjN94pobQSGZelb18XGN7kuwMk+Zwk7A",
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d/r0NTff1SQt+1GopZkT0nq6jF5Wh/oX+8iwtYjHvTxMpN1UQoXAvRF40O+EVg+Q3efJXh1t45cMco8EWU64VerOir+k7cQ3C9FtcgQw3kmz3s3HeVY10o13X/w6+rc8n6vXqxuIxYHnFxanxX8B6TgTMZNajNfVsmJV0aC1aezim7E2gsftc+6+zW5G+rCFaEsWV/IuSOUz0+Hh0U+7hzSrz9/4qXPEVmPy1f6Ll4hhquPAlXPVDwddqlJDYj7kmvzr1g3bKVpk+TtKDbWlVQDPaJx2DEI2jGkPYjhYb7okpTFKpUny94dZmFIQqCeSGPIniaq8Y+/CanugQ1ZRVQcThuXrTewqWhXcpVvkVHT9i4ImcpBl95HzCBXuiwSUv6FKvO25fp++w555rbn2piFtilrUwnkrZPW32jFuaQcKZF4mZwcLeH7POL5UCuS4TWyaKyArp7bRzXwWuIq1wPET2nAMUmUVL7ge2+tAevk1WOIsjLgSaz/g55wO3Yma7yhXRFKcnzTjS0hUQOZ3GfTNwCM4pjzAtIPzvVd4Fp0b1emWZS5WyOYdXsceEDi3c6WtkoHWOKhPU0zBzn8hA9TdlFFqKzf2QFbN5Zgg0gprDLnLWgpc3/ieI4C7ndEQ7ZeTNMXbT/Y10APFk3qO+IGkLXJ97/qTF41EXFDhlsL0",
|
||||
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
|
||||
"mac": "OibmACbORhI"
|
||||
}
|
||||
@@ -229,6 +212,37 @@ export const ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key that matches the public key in CURVE25519_KEY_BACKUP_DATA */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** base64-encoded backup decryption (private) key that does not match the public key in CURVE25519_KEY_BACKUP_DATA */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64_ALT = "dh4fP2LITyJusgnb0dEq/SQK253WGObvLxXF5FEX6qc";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
|
||||
* Contains the key from MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
|
||||
[MEGOLM_SESSION_DATA.session_id]: CURVE25519_KEY_BACKUP_DATA
|
||||
};
|
||||
|
||||
// Bob data
|
||||
|
||||
export const BOB_TEST_USER_ID = "@bob:xyz";
|
||||
@@ -338,26 +352,6 @@ export const BOB_ONE_TIME_KEYS = {
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
@@ -369,7 +363,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -380,7 +375,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -394,7 +390,8 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
};
|
||||
|
||||
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
|
||||
@@ -416,7 +413,7 @@ export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3IcGylnoUcS3doVkJZiprXytXMP89AKcgv5Dj7mS2ZdvNGE+Atro74bzZ5yot5BrE0ZE5SjoUBPLaLMMu9HopLIV+qx01Rc3F0wmkocSPo51N0nv6wvO5Cst0FiOGHDK6r1pFlgDEJLmBkOyC4e8oMVbKTJzsSQVbJ8tJ37xuhI+T5P0ZlmiqKDqYRp8uh50w+txLEixYhEUunFgCTt1DAmiS9pLNYhLyl1ggwuQjzZe+AV6timbRxNJy18/AEcPomJw7z/pxYIiNLHRKOC13Wp8kGWx9cOgfMQ5KmBuLS8psGiLTBkfWPLOfNYqjbeqAR+OGZQoS6hUjbBYU7QuFa4FOYBHkNB2UqNsdsMb9qB/qs7QGTSb8Lok5YjW1c81BUpmIyKvuqnKma0MZskrpTYGQD2eJDABFCZwLFm+LgDyUTeSiV5xguYztLrHOk8LHKo9M8dIZgoBjeFVJxyjbcXKsVS3aQkMXKCrRlKLqhZTws/ZJwVfW9DbktZ9dT+tRZQvI7tjJofojcLX61AGJDnqUf5+2Gv1tEnmUI953gIzc8NlcFabPOsDsZEODt7MdOCTPT3w29umyhKbCsslpb64LoS/AB2QRPRCgkJS7snRA",
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAA7TEpHlx8Ks23hCqXmVW710VjqK2K9xnWCyJvkHfE8x0w6AYvffDj+tRVP8C8M7t4849rD2itn0uma+YMkvjG/nANUTxG1dBf3oUOZ673vflCPoaz7s7x9ZNhYDVSVH5JTdMgNwwN42R5dqqxnGTu516tJzJh/9BWvyD9oIPWJ8X0rt1sbzEJ3PZeBXcSy8GTlZ1SgSFjeiXlwYxOZCaX2sxprk4N1oI1db6g+wCDBhbCGGucJIlTDJna/h9/C5J4drGd/fkisG3SidUmJXXCyInhs/BhwjGAtTGeQS8j7R8UnJxhMulYBHSckzj0Kas71LElPp8W8M4Jq81APA03n5UfYB+U6jbxjDgf8OJnxGQyrteq9F2+SEvS/TwHe1pE3t6EM2mDYRoYDTpU5pTNYSJkGIQMfWJKRxxuWUGs29o1twewJ6dhHgm+SlCII0M7ESoVdV54vxZCvHZnPcR0NXDzal7ils7zBKJmamHfPQBuaqNPU3KmSo+5R8ngFPaWU5LbWqYp/WxSBfNCoLZ7Jf8Io5uitjXTATR2qy2r6l/RJmk3RlfP51kliQqI2TWqRF96oaB96IGgUGSFCX/2pv0psOBGc1SjfmMB3d7gYis+2iBYVbG3xmnpeXbqvlD0Lw9TiTIPkjhJkTW1+lXyhy1xVH9ZmcFamcL7bX15Jx",
|
||||
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
|
||||
"mac": "lEfHlqfJQwU"
|
||||
}
|
||||
@@ -449,6 +446,34 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
|
||||
* Contains the key from BOB_MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const BOB_PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
|
||||
[BOB_MEGOLM_SESSION_DATA.session_id]: BOB_CURVE25519_KEY_BACKUP_DATA
|
||||
};
|
||||
|
||||
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
|
||||
+130
-16
@@ -1,9 +1,7 @@
|
||||
// load olm before the sdk if possible
|
||||
import "../olm-loader";
|
||||
import mkdebug from "debug";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type EventEmitter from "events";
|
||||
import { logger } from "../../src/logger";
|
||||
import {
|
||||
type IContent,
|
||||
type IEvent,
|
||||
@@ -15,6 +13,7 @@ import {
|
||||
import {
|
||||
ClientEvent,
|
||||
EventType,
|
||||
HistoryVisibility,
|
||||
type IJoinedRoom,
|
||||
type IPusher,
|
||||
type ISyncResponse,
|
||||
@@ -27,6 +26,8 @@ import { eventMapperFor } from "../../src/event-mapper";
|
||||
import { TEST_ROOM_ID } from "./test-data";
|
||||
import { KnownMembership, type Membership } from "../../src/@types/membership";
|
||||
|
||||
const debug = mkdebug("test-utils");
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
* SYNCING event.
|
||||
@@ -41,7 +42,7 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
|
||||
const p = new Promise<void>((resolve) => {
|
||||
const cb = (state: SyncState) => {
|
||||
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||
debug(`syncPromise(${count}): ${state}`);
|
||||
if (state === SyncState.Syncing) {
|
||||
resolve();
|
||||
} else {
|
||||
@@ -57,13 +58,22 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sync response which contains a single room (by default TEST_ROOM_ID), with the members given
|
||||
* @param roomMembers
|
||||
* @param roomId
|
||||
* Return a sync response which contains a single room (by default `TEST_ROOM_ID`), with the members given
|
||||
* and history visibility set to `shared`.
|
||||
*
|
||||
* @returns the sync response
|
||||
* @param roomMembers - An array of user IDs representing the members of the room.
|
||||
* @param roomHistoryVisibility - The history visibility setting for the room. Defaults to `shared`.
|
||||
* @param roomId - The ID of the room. Defaults to `TEST_ROOM_ID`.
|
||||
* @param encryptStateEvents - A boolean indicating whether state events should be encrypted. Defaults to `false`.
|
||||
*
|
||||
* @returns The sync response object containing the room data.
|
||||
*/
|
||||
export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): ISyncResponse {
|
||||
export function getSyncResponse(
|
||||
roomMembers: string[],
|
||||
roomHistoryVisibility: HistoryVisibility = HistoryVisibility.Shared,
|
||||
roomId = TEST_ROOM_ID,
|
||||
encryptStateEvents = false,
|
||||
): ISyncResponse {
|
||||
const roomResponse: IJoinedRoom = {
|
||||
summary: {
|
||||
"m.heroes": [],
|
||||
@@ -77,7 +87,16 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"io.element.msc4362.encrypt_state_events": encryptStateEvents,
|
||||
},
|
||||
}),
|
||||
mkEventCustom({
|
||||
sender: roomMembers[0],
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: roomHistoryVisibility,
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -131,7 +150,7 @@ export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
|
||||
// eslint-disable-line guard-for-in
|
||||
try {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jest.fn();
|
||||
result[key] = vi.fn();
|
||||
}
|
||||
} catch {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
@@ -522,25 +541,25 @@ export async function awaitDecryption(
|
||||
// already
|
||||
if (event.getClearContent() !== null) {
|
||||
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
|
||||
logger.log(`${Date.now()}: event ${event.getId()} got decryption error; waiting`);
|
||||
debug(`event ${event.getId()} got decryption error; waiting`);
|
||||
} else {
|
||||
return event;
|
||||
}
|
||||
} else {
|
||||
logger.log(`${Date.now()}: event ${event.getId()} is not yet decrypted; waiting`);
|
||||
debug(`event ${event.getId()} is not yet decrypted; waiting`);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (waitOnDecryptionFailure) {
|
||||
event.on(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
debug(`MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
if (!err) {
|
||||
resolve(ev);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
|
||||
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
debug(`MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
|
||||
resolve(ev);
|
||||
});
|
||||
}
|
||||
@@ -587,8 +606,103 @@ export async function advanceTimersUntil<T>(promise: Promise<T>): Promise<T> {
|
||||
});
|
||||
|
||||
while (!resolved) {
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
}
|
||||
|
||||
return await promise;
|
||||
}
|
||||
|
||||
export function jestFakeTimersAreEnabled(): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(setTimeout, "clock");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `callback` in a loop, until it returns a successful result (i.e. it does not throw), or we reach a timeout
|
||||
*
|
||||
* Based on the function of the same name in the {@link https://testing-library.com/docs/dom-testing-library/api-async/#waitfor DOM testing library}.
|
||||
*
|
||||
* @param callback - The function to call to check if we can proceed. If it returns a result (including a falsey one),
|
||||
* `waitFor` returns that result. If it throws, `waitFor` continues to wait.
|
||||
*
|
||||
* May return a promise, in which case no further checks are done until the promise resolves.
|
||||
*
|
||||
* @param timeout - The time to wait for, overall, in ms. If `callback` still hasn't returned a successful result after
|
||||
* this time, `waitFor` will throw an error.
|
||||
*
|
||||
* Defaults to 1000.
|
||||
*
|
||||
* @param interval - How often to call `callback`. Defaults to 50.
|
||||
*/
|
||||
export function waitFor<T>(
|
||||
callback: () => Promise<T> | T,
|
||||
{
|
||||
timeout = 1000,
|
||||
interval = 50,
|
||||
}: {
|
||||
timeout?: number;
|
||||
interval?: number;
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let lastError: any;
|
||||
let finished = false;
|
||||
let intervalId: ReturnType<typeof setTimeout> | undefined;
|
||||
let promisePending = false;
|
||||
|
||||
const overallTimeoutTimer = setTimeout(handleTimeout, timeout);
|
||||
const usingJestFakeTimers = jestFakeTimersAreEnabled();
|
||||
if (usingJestFakeTimers) {
|
||||
checkCallback();
|
||||
|
||||
while (!finished) {
|
||||
vi.advanceTimersByTime(interval);
|
||||
|
||||
// Could have timed-out
|
||||
if (finished) break;
|
||||
|
||||
checkCallback();
|
||||
}
|
||||
} else {
|
||||
intervalId = setInterval(checkCallback, interval);
|
||||
checkCallback();
|
||||
}
|
||||
|
||||
function checkCallback() {
|
||||
if (promisePending) {
|
||||
// still waiting for the previous check
|
||||
return;
|
||||
}
|
||||
|
||||
async function doCheck() {
|
||||
try {
|
||||
const result = await callback();
|
||||
onDone();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
// Save the most recent callback error to reject the promise with it in the event of a timeout
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
|
||||
promisePending = true;
|
||||
doCheck().finally(() => {
|
||||
promisePending = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onDone(): void {
|
||||
finished = true;
|
||||
clearTimeout(overallTimeoutTimer);
|
||||
if (intervalId !== undefined) clearInterval(intervalId);
|
||||
}
|
||||
|
||||
function handleTimeout() {
|
||||
onDone();
|
||||
if (lastError) {
|
||||
reject(lastError);
|
||||
} else {
|
||||
reject(new Error("Timed out in waitFor."));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+51
-92
@@ -17,21 +17,17 @@ limitations under the License.
|
||||
import {
|
||||
type ClientEvent,
|
||||
type ClientEventHandlerMap,
|
||||
type EmptyObject,
|
||||
EventType,
|
||||
type GroupCall,
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
type IContent,
|
||||
type ISendEventResponse,
|
||||
type MatrixClient,
|
||||
type MatrixEvent,
|
||||
type Room,
|
||||
RoomMember,
|
||||
type RoomState,
|
||||
RoomStateEvent,
|
||||
type RoomStateEventHandlerMap,
|
||||
type SendToDeviceContentMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
@@ -269,19 +265,20 @@ export class MockRTCRtpTransceiver {
|
||||
this.peerConn.needsNegotiation = true;
|
||||
}
|
||||
|
||||
public setCodecPreferences = jest.fn<void, RTCRtpCodec[]>();
|
||||
public setCodecPreferences = vi.fn<RTCRtpTransceiver["setCodecPreferences"]>();
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
export class MockMediaStreamTrack extends EventTarget {
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly kind: "audio" | "video",
|
||||
public enabled = true,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public stop = jest.fn<void, []>();
|
||||
public stop = vi.fn<() => void>();
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
public settings?: MediaTrackSettings;
|
||||
|
||||
@@ -289,45 +286,21 @@ export class MockMediaStreamTrack {
|
||||
return this.settings!;
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
|
||||
public typed(): MediaStreamTrack {
|
||||
return this as unknown as MediaStreamTrack;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
export class MockMediaStream {
|
||||
export class MockMediaStream extends EventTarget {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public listeners: [string, (...args: any[]) => any][] = [];
|
||||
public isStopped = false;
|
||||
|
||||
public dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
public getTracks() {
|
||||
return this.tracks;
|
||||
}
|
||||
@@ -337,17 +310,9 @@ export class MockMediaStream {
|
||||
public getVideoTracks() {
|
||||
return this.tracks.filter((track) => track.kind === "video");
|
||||
}
|
||||
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
public addTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
this.dispatchEvent(new Event("addtrack"));
|
||||
}
|
||||
public removeTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.splice(this.tracks.indexOf(track), 1);
|
||||
@@ -391,7 +356,7 @@ export class MockMediaHandler {
|
||||
public stopUserMediaStream(stream: MockMediaStream) {
|
||||
stream.isStopped = true;
|
||||
}
|
||||
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
|
||||
public getScreensharingStream = vi.fn((opts?: IScreensharingOpts) => {
|
||||
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
|
||||
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
|
||||
|
||||
@@ -416,19 +381,19 @@ export class MockMediaHandler {
|
||||
}
|
||||
|
||||
export class MockMediaDevices {
|
||||
public enumerateDevices = jest
|
||||
.fn<Promise<MediaDeviceInfo[]>, []>()
|
||||
public enumerateDevices = vi
|
||||
.fn<MediaDevices["enumerateDevices"]>()
|
||||
.mockResolvedValue([
|
||||
new MockMediaDeviceInfo("audioinput").typed(),
|
||||
new MockMediaDeviceInfo("videoinput").typed(),
|
||||
]);
|
||||
|
||||
public getUserMedia = jest
|
||||
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
|
||||
public getUserMedia = vi
|
||||
.fn<MediaDevices["getUserMedia"]>()
|
||||
.mockReturnValue(Promise.resolve(new MockMediaStream("local_stream").typed()));
|
||||
|
||||
public getDisplayMedia = jest
|
||||
.fn<Promise<MediaStream>, [MediaStreamConstraints]>()
|
||||
public getDisplayMedia = vi
|
||||
.fn<MediaDevices["getDisplayMedia"]>()
|
||||
.mockReturnValue(Promise.resolve(new MockMediaStream("local_display_stream").typed()));
|
||||
|
||||
public typed(): MediaDevices {
|
||||
@@ -462,14 +427,8 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
calls: new Map<string, MatrixCall>(),
|
||||
};
|
||||
|
||||
public sendStateEvent = jest.fn<
|
||||
Promise<ISendEventResponse>,
|
||||
[roomId: string, eventType: EventType, content: any, statekey: string]
|
||||
>();
|
||||
public sendToDevice = jest.fn<
|
||||
Promise<EmptyObject>,
|
||||
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
|
||||
>();
|
||||
public sendStateEvent = vi.fn<MatrixClient["sendStateEvent"]>();
|
||||
public sendToDevice = vi.fn<MatrixClient["sendToDevice"]>();
|
||||
|
||||
public isInitialSyncComplete(): boolean {
|
||||
return false;
|
||||
@@ -499,11 +458,11 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
public getUseE2eForGroupCall = () => false;
|
||||
public checkTurnServers = () => null;
|
||||
|
||||
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
|
||||
public getSyncState = vi.fn<MatrixClient["getSyncState"]>().mockReturnValue(SyncState.Syncing);
|
||||
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
public getFoci = jest.fn();
|
||||
public getRooms = vi.fn<MatrixClient["getRooms"]>().mockReturnValue([]);
|
||||
public getRoom = vi.fn();
|
||||
public getFoci = vi.fn();
|
||||
|
||||
public supportsThreads(): boolean {
|
||||
return true;
|
||||
@@ -534,20 +493,20 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
public opponentMember = { userId: this.opponentUserId };
|
||||
public callId = "1";
|
||||
public localUsermediaFeed = {
|
||||
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
|
||||
isAudioMuted: jest.fn().mockReturnValue(false),
|
||||
isVideoMuted: jest.fn().mockReturnValue(false),
|
||||
setAudioVideoMuted: vi.fn<CallFeed["setAudioVideoMuted"]>(),
|
||||
isAudioMuted: vi.fn().mockReturnValue(false),
|
||||
isVideoMuted: vi.fn().mockReturnValue(false),
|
||||
stream: new MockMediaStream("stream"),
|
||||
} as unknown as CallFeed;
|
||||
public remoteUsermediaFeed?: CallFeed;
|
||||
public remoteScreensharingFeed?: CallFeed;
|
||||
|
||||
public reject = jest.fn<void, []>();
|
||||
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
|
||||
public hangup = jest.fn<void, []>();
|
||||
public initStats = jest.fn<void, []>();
|
||||
public reject = vi.fn<() => void>();
|
||||
public answerWithCallFeeds = vi.fn<MatrixCall["answerWithCallFeeds"]>();
|
||||
public hangup = vi.fn<() => void>();
|
||||
public initStats = vi.fn<() => void>();
|
||||
|
||||
public sendMetadataUpdate = jest.fn<void, []>();
|
||||
public sendMetadataUpdate = vi.fn<() => void>();
|
||||
|
||||
public getOpponentMember(): Partial<RoomMember> {
|
||||
return this.opponentMember;
|
||||
@@ -586,11 +545,11 @@ export class MockCallFeed {
|
||||
}
|
||||
|
||||
export function installWebRTCMocks() {
|
||||
globalThis.navigator = {
|
||||
vi.stubGlobal("navigator", {
|
||||
mediaDevices: new MockMediaDevices().typed(),
|
||||
} as unknown as Navigator;
|
||||
});
|
||||
|
||||
globalThis.window = {
|
||||
vi.stubGlobal("window", {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
@@ -598,16 +557,16 @@ export function installWebRTCMocks() {
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
globalThis.document = {};
|
||||
});
|
||||
|
||||
vi.stubGlobal("document", {});
|
||||
|
||||
// @ts-ignore Mock
|
||||
globalThis.AudioContext = MockAudioContext;
|
||||
|
||||
// @ts-ignore Mock
|
||||
globalThis.RTCRtpReceiver = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
getCapabilities: vi.fn().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
@@ -615,7 +574,7 @@ export function installWebRTCMocks() {
|
||||
|
||||
// @ts-ignore Mock
|
||||
globalThis.RTCRtpSender = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
getCapabilities: vi.fn().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
}),
|
||||
@@ -632,22 +591,22 @@ export function makeMockGroupCallStateEvent(
|
||||
redacted?: boolean,
|
||||
): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue(content),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
|
||||
getType: vi.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
getRoomId: vi.fn().mockReturnValue(roomId),
|
||||
getTs: vi.fn().mockReturnValue(0),
|
||||
getContent: vi.fn().mockReturnValue(content),
|
||||
getStateKey: vi.fn().mockReturnValue(groupCallId),
|
||||
isRedacted: vi.fn().mockReturnValue(redacted ?? false),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getRoomId: jest.fn().mockReturnValue(roomId),
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getRoomId: vi.fn().mockReturnValue(roomId),
|
||||
getTs: vi.fn().mockReturnValue(0),
|
||||
getContent: vi.fn().mockReturnValue({}),
|
||||
getStateKey: vi.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
|
||||
});
|
||||
|
||||
it("should have a falsey unstable if needed", () => {
|
||||
const ns = new NamespacedValue("stable");
|
||||
const ns = new NamespacedValue("stable", null);
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.stable]);
|
||||
@@ -61,7 +61,7 @@ describe("UnstableValue", () => {
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new UnstableValue(null!, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(<any>ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.unstable]);
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("ReEmitter", function () {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const handler = jest.fn();
|
||||
const handler = vi.fn();
|
||||
tgt.on(EVENTNAME, handler);
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
@@ -61,7 +61,7 @@ describe("ReEmitter", function () {
|
||||
// without the workaround in ReEmitter, this would throw
|
||||
src.doAnError();
|
||||
|
||||
const handler = jest.fn();
|
||||
const handler = vi.fn();
|
||||
tgt.on("error", handler);
|
||||
|
||||
src.doAnError();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getMockClientWithEventEmitter } from "../test-utils/client";
|
||||
import { StubStore } from "../../src/store/stub";
|
||||
import { type IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { logger } from "../../src/logger.ts";
|
||||
|
||||
describe("onResumedSync", () => {
|
||||
let batch: IndexedToDeviceBatch | null;
|
||||
@@ -34,16 +35,16 @@ describe("onResumedSync", () => {
|
||||
};
|
||||
|
||||
store = new StubStore();
|
||||
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
|
||||
store.getOldestToDeviceBatch = vi.fn().mockImplementation(() => {
|
||||
return batch;
|
||||
});
|
||||
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
|
||||
store.removeToDeviceBatch = vi.fn().mockImplementation(() => {
|
||||
batch = null;
|
||||
});
|
||||
|
||||
mockClient = getMockClientWithEventEmitter({});
|
||||
mockClient.store = store;
|
||||
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
|
||||
mockClient.sendToDevice = vi.fn().mockImplementation(async () => {
|
||||
if (shouldFailSendToDevice) {
|
||||
await Promise.reject(new ConnectionError("")).finally(() => {
|
||||
setTimeout(onSendToDeviceFailure, 0);
|
||||
@@ -55,7 +56,7 @@ describe("onResumedSync", () => {
|
||||
}
|
||||
});
|
||||
|
||||
queue = new ToDeviceMessageQueue(mockClient);
|
||||
queue = new ToDeviceMessageQueue(mockClient, logger);
|
||||
});
|
||||
|
||||
it("resends queue after connectivity restored", async () => {
|
||||
|
||||
@@ -35,6 +35,7 @@ describe("AutoDiscovery", function () {
|
||||
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @vitest/expect-expect
|
||||
it("should throw an error when no domain is specified", function () {
|
||||
getHttpBackend();
|
||||
return Promise.all([
|
||||
@@ -190,7 +191,7 @@ describe("AutoDiscovery", function () {
|
||||
};
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then(expect(expected).toEqual),
|
||||
AutoDiscovery.findClientConfig("example.org").then((config) => expect(config).toEqual(expected)),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
|
||||
describe("makeBeaconInfoContent()", () => {
|
||||
const mockDateNow = 123456789;
|
||||
beforeEach(() => {
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
|
||||
vi.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
vi.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
it("create fully defined event content", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
|
||||
@@ -183,39 +183,43 @@ describe("Topic content helpers", () => {
|
||||
it("creates fully defined event content without html", () => {
|
||||
expect(makeTopicContent("pizza")).toEqual({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("creates fully defined event content with html", () => {
|
||||
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("creates an empty event when the topic is falsey", () => {
|
||||
expect(makeTopicContent(undefined)).toEqual({
|
||||
topic: undefined,
|
||||
[M_TOPIC.name]: [],
|
||||
[M_TOPIC.name]: { "m.text": [] },
|
||||
});
|
||||
expect(makeTopicContent(null)).toEqual({
|
||||
topic: null,
|
||||
[M_TOPIC.name]: [],
|
||||
[M_TOPIC.name]: { "m.text": [] },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -225,11 +229,13 @@ describe("Topic content helpers", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "pizza",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
@@ -240,12 +246,14 @@ describe("Topic content helpers", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
@@ -256,12 +264,14 @@ describe("Topic content helpers", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
@@ -279,11 +289,34 @@ describe("Topic content helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses legacy event content when new topic key is invalid", () => {
|
||||
// TODO delete this test and re-enable the next one after support for the invalid form is removed
|
||||
// https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
|
||||
it("parses malformed event content with html topic", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
"topic": "pizza",
|
||||
"m.topic": {} as any,
|
||||
"m.topic": [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
] as any,
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
html: "<b>pizza</b>",
|
||||
});
|
||||
});
|
||||
it.skip("uses legacy event content when new topic key is invalid", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
"topic": "pizza",
|
||||
"m.topic": [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
] as any,
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
|
||||
@@ -14,8 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
import "jest-localstorage-mock";
|
||||
import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from "../../../../src";
|
||||
import { type CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
|
||||
|
||||
|
||||
@@ -29,12 +29,7 @@ describe("sha256", () => {
|
||||
});
|
||||
|
||||
it("throws if webcrypto is not available", async () => {
|
||||
const oldCrypto = globalThis.crypto;
|
||||
try {
|
||||
globalThis.crypto = {} as any;
|
||||
await expect(sha256("test")).rejects.toThrow();
|
||||
} finally {
|
||||
globalThis.crypto = oldCrypto;
|
||||
}
|
||||
vi.stubGlobal("crypto", {});
|
||||
await expect(sha256("test")).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
+268
-51
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -22,7 +18,7 @@ limitations under the License.
|
||||
// project, which doesn't know about our TypeEventEmitter implementation at all
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EventEmitter } from "events";
|
||||
import { type MockedObject } from "jest-mock";
|
||||
import { type MockedObject } from "vitest";
|
||||
import {
|
||||
type WidgetApi,
|
||||
WidgetApiToWidgetAction,
|
||||
@@ -36,7 +32,14 @@ import {
|
||||
type IRoomEvent,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import {
|
||||
createRoomWidgetClient,
|
||||
EventType,
|
||||
type IEvent,
|
||||
MatrixError,
|
||||
MsgType,
|
||||
UpdateDelayedEventAction,
|
||||
} from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
|
||||
@@ -46,6 +49,7 @@ import { sleep } from "../../src/utils";
|
||||
import { SlidingSync } from "../../src/sliding-sync";
|
||||
import { logger } from "../../src/logger";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
|
||||
|
||||
const testOIDCToken = {
|
||||
access_token: "12345678",
|
||||
@@ -53,27 +57,28 @@ const testOIDCToken = {
|
||||
matrix_server_name: "homeserver.oabc",
|
||||
token_type: "Bearer",
|
||||
};
|
||||
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = jest.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = jest.fn();
|
||||
public requestCapability = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilities = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendMessage = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveMessage = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendState = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveState = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public sendRoomEvent = jest.fn(
|
||||
public start = vi.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = vi.fn();
|
||||
public requestCapability = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilities = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityForRoomTimeline = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendMessage = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveMessage = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendState = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveState = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToSendToDevice = vi.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityToReceiveToDevice = vi.fn().mockResolvedValue(undefined);
|
||||
public sendRoomEvent = vi.fn(
|
||||
async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
|
||||
delay === undefined && parentDelayId === undefined
|
||||
? { event_id: `$${Math.random()}` }
|
||||
: { delay_id: `id-${Math.random()}` },
|
||||
);
|
||||
public sendStateEvent = jest.fn(
|
||||
public sendStateEvent = vi.fn(
|
||||
async (
|
||||
eventType: string,
|
||||
stateKey: string,
|
||||
@@ -86,22 +91,24 @@ class MockWidgetApi extends EventEmitter {
|
||||
? { event_id: `$${Math.random()}` }
|
||||
: { delay_id: `id-${Math.random()}` },
|
||||
);
|
||||
public updateDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public requestOpenIDConnectToken = jest.fn(async () => {
|
||||
public cancelScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public restartScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public sendScheduledDelayedEvent = vi.fn().mockResolvedValue(undefined);
|
||||
public sendToDevice = vi.fn().mockResolvedValue(undefined);
|
||||
public requestOpenIDConnectToken = vi.fn(async () => {
|
||||
return testOIDCToken;
|
||||
return new Promise<IOpenIDCredentials>(() => {
|
||||
return testOIDCToken;
|
||||
});
|
||||
});
|
||||
public readStateEvents = jest.fn(async () => []);
|
||||
public getTurnServers = jest.fn(async () => []);
|
||||
public sendContentLoaded = jest.fn().mockResolvedValue(undefined);
|
||||
public readStateEvents = vi.fn(async () => []);
|
||||
public getTurnServers = vi.fn(async () => []);
|
||||
public sendContentLoaded = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
public transport = {
|
||||
reply: jest.fn(),
|
||||
send: jest.fn(),
|
||||
sendComplete: jest.fn(),
|
||||
reply: vi.fn(),
|
||||
send: vi.fn(),
|
||||
sendComplete: vi.fn(),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -169,6 +176,9 @@ describe("RoomWidgetClient", () => {
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -228,7 +238,7 @@ describe("RoomWidgetClient", () => {
|
||||
);
|
||||
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 injectSpy = vi.spyOn((client as any).syncApi, "injectRoomEvents");
|
||||
const widgetSendEmitter = new EventEmitter();
|
||||
const widgetSendPromise = new Promise<void>((resolve) =>
|
||||
widgetSendEmitter.once("send", () => resolve()),
|
||||
@@ -355,10 +365,10 @@ describe("RoomWidgetClient", () => {
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to send");
|
||||
widgetApi.transport.send.mockRejectedValue(error);
|
||||
vi.mocked(widgetApi.transport.send).mockRejectedValue(error);
|
||||
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
|
||||
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
@@ -381,22 +391,22 @@ describe("RoomWidgetClient", () => {
|
||||
response: errorData,
|
||||
},
|
||||
});
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl, undefined, expect.any(Headers));
|
||||
|
||||
widgetApi.transport.send.mockRejectedValue(widgetError);
|
||||
vi.mocked(widgetApi.transport.send).mockRejectedValue(widgetError);
|
||||
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send as any);
|
||||
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
).rejects.toThrow(matrixError);
|
||||
).rejects.toStrictEqual(matrixError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delayed events", () => {
|
||||
describe("when supported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
|
||||
Promise.resolve(feature === "org.matrix.msc4140"),
|
||||
);
|
||||
|
||||
@@ -424,6 +434,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
2000,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -444,6 +455,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
parentDelayId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -531,21 +543,63 @@ describe("RoomWidgetClient", () => {
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("updates delayed events", async () => {
|
||||
it.each([UpdateDelayedEventAction.Cancel, UpdateDelayedEventAction.Restart, UpdateDelayedEventAction.Send])(
|
||||
"can %s scheduled delayed events (action in parameter)",
|
||||
async (action: UpdateDelayedEventAction) => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(
|
||||
MatrixCapabilities.MSC4157UpdateDelayedEvent,
|
||||
);
|
||||
await client._unstable_updateDelayedEvent("id", action);
|
||||
let updateDelayedEvent: (delayId: string) => Promise<unknown>;
|
||||
switch (action) {
|
||||
case UpdateDelayedEventAction.Cancel:
|
||||
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
|
||||
break;
|
||||
case UpdateDelayedEventAction.Restart:
|
||||
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
|
||||
break;
|
||||
case UpdateDelayedEventAction.Send:
|
||||
updateDelayedEvent = widgetApi.sendScheduledDelayedEvent;
|
||||
break;
|
||||
}
|
||||
expect(updateDelayedEvent).toHaveBeenCalledWith("id");
|
||||
},
|
||||
);
|
||||
|
||||
it("can cancel scheduled delayed events (action in method)", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
for (const action of [
|
||||
UpdateDelayedEventAction.Cancel,
|
||||
UpdateDelayedEventAction.Restart,
|
||||
UpdateDelayedEventAction.Send,
|
||||
]) {
|
||||
await client._unstable_updateDelayedEvent("id", action);
|
||||
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
|
||||
}
|
||||
await client._unstable_cancelScheduledDelayedEvent("id");
|
||||
expect(widgetApi.cancelScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||
});
|
||||
|
||||
it("can restart scheduled delayed events (action in method)", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
await client._unstable_restartScheduledDelayedEvent("id");
|
||||
expect(widgetApi.restartScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||
});
|
||||
|
||||
it("can send scheduled delayed events (action in method)", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
await client._unstable_sendScheduledDelayedEvent("id");
|
||||
expect(widgetApi.sendScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsupported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("fails to send delayed message events", async () => {
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
await expect(
|
||||
@@ -583,6 +637,13 @@ describe("RoomWidgetClient", () => {
|
||||
"Server does not support",
|
||||
);
|
||||
}
|
||||
for (const updateDelayedEvent of [
|
||||
client._unstable_cancelScheduledDelayedEvent,
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
client._unstable_sendScheduledDelayedEvent,
|
||||
]) {
|
||||
await expect(updateDelayedEvent.call(client, "id")).rejects.toThrow("Server does not support");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -729,7 +790,7 @@ describe("RoomWidgetClient", () => {
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
|
||||
const logSpy = jest.spyOn(logger, "error");
|
||||
const logSpy = vi.spyOn(logger, "error");
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
@@ -808,6 +869,162 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky events", () => {
|
||||
describe("when supported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
|
||||
Promise.resolve(feature === "org.matrix.msc4354"),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests capabilities when set", async () => {
|
||||
await makeClient({ sendSticky: true, receiveSticky: true });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
});
|
||||
|
||||
it("does not request capabilities when unset", async () => {
|
||||
await makeClient({});
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(
|
||||
MatrixCapabilities.MSC4407ReceiveStickyEvent,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith(EventType.RTCMembership);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
await client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
});
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
EventType.RTCMembership,
|
||||
{ msc4354_sticky_key: "test" },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
2000,
|
||||
);
|
||||
});
|
||||
|
||||
it("receives (adds, updates, then removes when redacted)", async () => {
|
||||
await makeClient({ receiveEvent: [EventType.RTCMembership, EventType.RoomRedaction] });
|
||||
const room = client.getRoom("!1:example.org")!;
|
||||
|
||||
function expectStickyEvents(events: IEvent[]) {
|
||||
expect([...room._unstable_getStickyEvents()].map((e) => e.getEffectiveEvent())).toEqual(events);
|
||||
}
|
||||
|
||||
async function sendAndExpectStickyUpdate(
|
||||
eventToSend: IEvent,
|
||||
added: IEvent[],
|
||||
updated: { current: IEvent; previous: IEvent }[],
|
||||
removed: IEvent[],
|
||||
) {
|
||||
const emittedStickyUpdate = new Promise<
|
||||
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
|
||||
>((resolve) => room.once(RoomStickyEventsEvent.Update, (...args) => resolve(args)));
|
||||
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
|
||||
detail: { data: eventToSend },
|
||||
}),
|
||||
);
|
||||
|
||||
const [addedReceived, updatedReceived, removedReceived] = await emittedStickyUpdate;
|
||||
expect(addedReceived.map((e) => e.getEffectiveEvent())).toEqual(added);
|
||||
expect(
|
||||
updatedReceived.map(({ current, previous }) => ({
|
||||
current: current.getEffectiveEvent(),
|
||||
previous: previous.getEffectiveEvent(),
|
||||
})),
|
||||
).toEqual(updated);
|
||||
expect(removedReceived.map((e) => e.getEffectiveEvent())).toEqual(removed);
|
||||
}
|
||||
|
||||
// First, add a new sticky event to the map. The client should emit.
|
||||
const event1 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$pduhfiidph",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event1, [event1], [], []);
|
||||
// It should remain cached in the sticky map
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Next, update the same key in the sticky map
|
||||
const event2 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$zshgyutptfh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event2, [], [{ current: event2, previous: event1 }], []);
|
||||
expectStickyEvents([event2]);
|
||||
|
||||
// Next, redact the second event. Because it has the first as a predecessor, the map should revert to
|
||||
// the first event.
|
||||
const redaction1 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$cimoexnvz",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event2.event_id,
|
||||
content: { redacts: event2.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction1, [], [{ current: event1, previous: event2 }], []);
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Finally, redact the first event. Now everything should be gone from the map.
|
||||
const redaction2 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$drgzmenlh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event1.event_id,
|
||||
content: { redacts: event1.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction2, [], [], [event1]);
|
||||
expectStickyEvents([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsupported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("fails to send", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
await expect(
|
||||
client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
}),
|
||||
).rejects.toThrow("Server does not support");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = new Map([
|
||||
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
||||
@@ -929,7 +1146,7 @@ describe("RoomWidgetClient", () => {
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to get token");
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(error);
|
||||
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(error);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
@@ -953,9 +1170,9 @@ describe("RoomWidgetClient", () => {
|
||||
response: errorData,
|
||||
},
|
||||
});
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl, undefined, expect.any(Headers));
|
||||
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
|
||||
vi.mocked(widgetApi.transport.sendComplete).mockRejectedValue(widgetError);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("eventMapperFor", function () {
|
||||
setUserCreator(_) {},
|
||||
} as IStore,
|
||||
scheduler: {
|
||||
setProcessFunction: jest.fn(),
|
||||
setProcessFunction: vi.fn(),
|
||||
} as unknown as MatrixScheduler,
|
||||
userId: userId,
|
||||
});
|
||||
@@ -133,7 +133,7 @@ describe("eventMapperFor", function () {
|
||||
event_id: eventId,
|
||||
};
|
||||
|
||||
const decryptEventIfNeededSpy = jest.spyOn(client, "decryptEventIfNeeded");
|
||||
const decryptEventIfNeededSpy = vi.spyOn(client, "decryptEventIfNeeded");
|
||||
decryptEventIfNeededSpy.mockResolvedValue(); // stub it out
|
||||
|
||||
const mapper = eventMapperFor(client, {
|
||||
@@ -161,7 +161,7 @@ describe("eventMapperFor", function () {
|
||||
event_id: eventId,
|
||||
};
|
||||
|
||||
const evListener = jest.fn();
|
||||
const evListener = vi.fn();
|
||||
client.on(MatrixEventEvent.Replaced, evListener);
|
||||
|
||||
const noReEmitMapper = eventMapperFor(client, {
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { type MockInstance } from "vitest";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
@@ -196,7 +196,7 @@ describe("EventTimelineSet", () => {
|
||||
|
||||
it("should aggregate relations which belong to unknown timeline without adding them to any timeline", () => {
|
||||
// If threads are disabled all events go into the main timeline
|
||||
mocked(client.supportsThreads).mockReturnValue(true);
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
const reactionEvent = utils.mkReaction(messageEvent, client, client.getSafeUserId(), roomId);
|
||||
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
@@ -228,7 +228,7 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
@@ -306,20 +306,20 @@ describe("EventTimelineSet", () => {
|
||||
});
|
||||
|
||||
describe("with events to be decrypted", () => {
|
||||
let messageEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let messageEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
let messageEventShouldAttemptDecryptionSpy: MockInstance;
|
||||
let messageEventIsDecryptionFailureSpy: MockInstance;
|
||||
|
||||
let replyEventShouldAttemptDecryptionSpy: jest.SpyInstance;
|
||||
let replyEventIsDecryptionFailureSpy: jest.SpyInstance;
|
||||
let replyEventShouldAttemptDecryptionSpy: MockInstance;
|
||||
let replyEventIsDecryptionFailureSpy: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
messageEventShouldAttemptDecryptionSpy = jest.spyOn(messageEvent, "shouldAttemptDecryption");
|
||||
messageEventShouldAttemptDecryptionSpy = vi.spyOn(messageEvent, "shouldAttemptDecryption");
|
||||
messageEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
messageEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
||||
messageEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
|
||||
|
||||
replyEventShouldAttemptDecryptionSpy = jest.spyOn(replyEvent, "shouldAttemptDecryption");
|
||||
replyEventShouldAttemptDecryptionSpy = vi.spyOn(replyEvent, "shouldAttemptDecryption");
|
||||
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
|
||||
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
|
||||
replyEventIsDecryptionFailureSpy = vi.spyOn(messageEvent, "isDecryptionFailure");
|
||||
|
||||
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
|
||||
});
|
||||
@@ -384,7 +384,7 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
vi.mocked(client.supportsThreads).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { Direction, EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
@@ -20,21 +18,21 @@ describe("EventTimeline", function () {
|
||||
const getTimeline = (): EventTimeline => {
|
||||
const room = new Room(roomId, mockClient, userA);
|
||||
const timelineSet = new EventTimelineSet(room);
|
||||
jest.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
||||
vi.spyOn(room, "getUnfilteredTimelineSet").mockReturnValue(timelineSet);
|
||||
|
||||
const timeline = new EventTimeline(timelineSet);
|
||||
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
|
||||
// otherwise the default member property values (e.g. paginationToken) will be incorrect
|
||||
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
|
||||
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
|
||||
timeline.getState(Direction.Backward)!.setStateEvents = vi.fn();
|
||||
timeline.getState(Direction.Backward)!.getSentinelMember = vi.fn();
|
||||
timeline.getState(Direction.Forward)!.setStateEvents = vi.fn();
|
||||
timeline.getState(Direction.Forward)!.getSentinelMember = vi.fn();
|
||||
return timeline;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
// reset any RoomState mocks
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
|
||||
timeline = getTimeline();
|
||||
});
|
||||
@@ -67,12 +65,12 @@ describe("EventTimeline", function () {
|
||||
timeline.initialiseState(events);
|
||||
// @ts-ignore private prop
|
||||
const timelineStartState = timeline.startState!;
|
||||
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
expect(vi.mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
// @ts-ignore private prop
|
||||
const timelineEndState = timeline.endState!;
|
||||
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
expect(vi.mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(events, {
|
||||
timelineWasEmpty: undefined,
|
||||
});
|
||||
});
|
||||
@@ -210,13 +208,13 @@ describe("EventTimeline", function () {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -253,13 +251,13 @@ describe("EventTimeline", function () {
|
||||
sentinel.name = "Old Alice";
|
||||
sentinel.membership = KnownMembership.Join;
|
||||
|
||||
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
vi.mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember.mockImplementation(function (uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
|
||||
exports[`MatrixHttpApi > should return expected object from \`getContentUri\` 1`] = `
|
||||
{
|
||||
"base": "http://baseUrl",
|
||||
"params": {
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("MatrixError", () => {
|
||||
|
||||
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);
|
||||
vi.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
|
||||
|
||||
+196
-103
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Mocked } from "jest-mock";
|
||||
import { type Mocked, type MockedFunction } from "vitest";
|
||||
|
||||
import { FetchHttpApi } from "../../../src/http-api/fetch";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type IHttpOpts,
|
||||
MatrixError,
|
||||
Method,
|
||||
TokenRefreshError,
|
||||
} from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { type QueryDict, sleep } from "../../../src/utils";
|
||||
@@ -39,42 +40,42 @@ describe("FetchHttpApi", () => {
|
||||
const tokenInactiveError = new MatrixError({ errcode: "M_UNKNOWN_TOKEN", error: "Token is not active" }, 401);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should support aborting multiple times", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
|
||||
api.request(Method.Get, "/foo");
|
||||
api.request(Method.Get, "/baz");
|
||||
expect(fetchFn.mock.calls[0][0].href.endsWith("/foo")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[1][0].href.endsWith("/baz")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[0][0] as URL).href.endsWith("/foo")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[1][0] as URL).href.endsWith("/baz")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeFalsy();
|
||||
|
||||
api.abort();
|
||||
expect(fetchFn.mock.calls[0][1].signal.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1].signal.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[0][1]?.signal?.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[1][1]?.signal?.aborted).toBeTruthy();
|
||||
|
||||
api.request(Method.Get, "/bar");
|
||||
expect(fetchFn.mock.calls[2][0].href.endsWith("/bar")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[2][0] as URL).href.endsWith("/bar")).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeFalsy();
|
||||
|
||||
api.abort();
|
||||
expect(fetchFn.mock.calls[2][1].signal.aborted).toBeTruthy();
|
||||
expect(fetchFn.mock.calls[2][1]?.signal?.aborted).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should fall back to global fetch if fetchFn not provided", () => {
|
||||
globalThis.fetch = jest.fn();
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const spy = (globalThis.fetch = vi.fn());
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
api.fetch("test");
|
||||
expect(globalThis.fetch).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update identity server base url", () => {
|
||||
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
expect(api.opts.idBaseUrl).toBeUndefined();
|
||||
api.setIdBaseUrl("https://id.foo.bar");
|
||||
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
|
||||
@@ -82,153 +83,213 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
describe("idServerRequest", () => {
|
||||
it("should throw if no idBaseUrl", () => {
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2)).toThrow(
|
||||
"No identity server base URL set",
|
||||
);
|
||||
});
|
||||
|
||||
it("should send params as query string for GET requests", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
idBaseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
onlyData: true,
|
||||
});
|
||||
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).toBe("bar");
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.getAll("via")).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should send params as body for non-GET requests", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
idBaseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
onlyData: true,
|
||||
});
|
||||
const params = { foo: "bar", via: ["a", "b"] };
|
||||
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
|
||||
expect(JSON.parse(fetchFn.mock.calls[0][1].body)).toStrictEqual(params);
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("foo")).not.toBe("bar");
|
||||
expect(JSON.parse(fetchFn.mock.calls[0][1]!.body as string)).toStrictEqual(params);
|
||||
});
|
||||
|
||||
it("should add Authorization header if token provided", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
idBaseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
onlyData: true,
|
||||
});
|
||||
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>).Authorization).toBe("Bearer token");
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the Response object if onlyData=false", async () => {
|
||||
const res = { ok: true };
|
||||
const fetchFn = jest.fn().mockResolvedValue(res);
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
|
||||
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
|
||||
it("should complain if constructed without `onlyData: true`", async () => {
|
||||
expect(
|
||||
() =>
|
||||
new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
}),
|
||||
).toThrow("Constructing FetchHttpApi without `onlyData=true` is no longer supported.");
|
||||
});
|
||||
|
||||
it("should return text if json=false", async () => {
|
||||
it("should set an Accept header, and parse the response as JSON, by default", async () => {
|
||||
const result = { a: 1 };
|
||||
const fetchFn = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue(result) });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(result);
|
||||
expect(fetchFn.mock.calls[0][1].headers.Accept).toBe("application/json");
|
||||
});
|
||||
|
||||
it("should not set an Accept header, and should return text if json=false", async () => {
|
||||
const text = "418 I'm a teapot";
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, text: jest.fn().mockResolvedValue(text) });
|
||||
const fetchFn = vi.fn().mockResolvedValue({ ok: true, text: vi.fn().mockResolvedValue(text) });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await expect(
|
||||
api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
||||
json: false,
|
||||
}),
|
||||
).resolves.toBe(text);
|
||||
expect(fetchFn.mock.calls[0][1].headers.Accept).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("should not set an Accept header, and should return a blob, if rawResponseBody is true", async () => {
|
||||
const blob = new Blob(["blobby"]);
|
||||
const fetchFn = vi.fn().mockResolvedValue({ ok: true, blob: vi.fn().mockResolvedValue(blob) });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await expect(
|
||||
api.requestOtherUrl(Method.Get, "http://url", undefined, {
|
||||
rawResponseBody: true,
|
||||
}),
|
||||
).resolves.toBe(blob);
|
||||
expect(fetchFn.mock.calls[0][1].headers.Accept).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("should throw an error if both `json` and `rawResponseBody` are defined", async () => {
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn: vi.fn(),
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(
|
||||
api.requestOtherUrl(Method.Get, "http://url", undefined, { rawResponseBody: false, json: true }),
|
||||
).rejects.toThrow("Invalid call to `FetchHttpApi`");
|
||||
});
|
||||
|
||||
it("should send token via query params if useAuthorizationHeader=false", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBe("token");
|
||||
});
|
||||
|
||||
it("should send token via headers by default", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should not send a token if not calling `authedRequest`", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
onlyData: true,
|
||||
});
|
||||
api.request(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should ensure no token is leaked out via query params if sending via headers", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: true,
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path", { access_token: "123" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBeFalsy();
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via query params", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
|
||||
expect((fetchFn.mock.calls[0][0] as URL).searchParams.get("access_token")).toBe("RealToken");
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via header", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: true,
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Authorization: "Bearer RealToken" },
|
||||
});
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer RealToken");
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Authorization"]).toBe("Bearer RealToken");
|
||||
});
|
||||
|
||||
it("should not override Accept header", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Accept: "text/html" },
|
||||
});
|
||||
expect(fetchFn.mock.calls[0][1].headers["Accept"]).toBe("text/html");
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>)["Accept"]).toBe("text/html");
|
||||
});
|
||||
|
||||
it("should emit NoConsent when given errcode=M_CONTENT_NOT_GIVEN", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({
|
||||
const fetchFn = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
text: jest.fn().mockResolvedValue(
|
||||
text: vi.fn().mockResolvedValue(
|
||||
JSON.stringify({
|
||||
errcode: "M_CONSENT_NOT_GIVEN",
|
||||
error: "Ye shall ask for consent",
|
||||
@@ -236,7 +297,7 @@ describe("FetchHttpApi", () => {
|
||||
),
|
||||
});
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
|
||||
await Promise.all([
|
||||
emitPromise(emitter, HttpApiEvent.NoConsent),
|
||||
@@ -246,11 +307,11 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
describe("authedRequest", () => {
|
||||
it("should not include token if unset", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await api.authedRequest(Method.Post, "/account/password");
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
|
||||
expect((fetchFn.mock.calls[0][1]!.headers as Record<string, any>).Authorization).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("with refresh token", () => {
|
||||
@@ -263,7 +324,13 @@ describe("FetchHttpApi", () => {
|
||||
error: "Token is not active",
|
||||
soft_logout: false,
|
||||
};
|
||||
const unknownTokenErr = new MatrixError(unknownTokenErrBody, 401);
|
||||
const unknownTokenErr = new MatrixError(
|
||||
unknownTokenErrBody,
|
||||
401,
|
||||
undefined,
|
||||
undefined,
|
||||
expect.anything(),
|
||||
);
|
||||
const unknownTokenResponse = {
|
||||
ok: false,
|
||||
status: 401,
|
||||
@@ -272,19 +339,27 @@ describe("FetchHttpApi", () => {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
text: jest.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
|
||||
text: vi.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
|
||||
};
|
||||
const okayResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: vi.fn().mockResolvedValue({ x: 1 }),
|
||||
};
|
||||
|
||||
describe("without a tokenRefreshFunction", () => {
|
||||
it("should emit logout and throw", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, accessToken, refreshToken });
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
);
|
||||
@@ -295,10 +370,10 @@ describe("FetchHttpApi", () => {
|
||||
describe("with a tokenRefreshFunction", () => {
|
||||
it("should emit logout and throw when token refresh fails", async () => {
|
||||
const error = new MatrixError();
|
||||
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -306,6 +381,7 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
@@ -316,10 +392,10 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
it("should not emit logout but still throw when token refresh fails due to transitive fault", async () => {
|
||||
const error = new ConnectionError("transitive fault");
|
||||
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const tokenRefreshFunction = vi.fn().mockRejectedValue(error);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -327,9 +403,10 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
new TokenRefreshError(unknownTokenErr),
|
||||
);
|
||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||
expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
|
||||
@@ -338,16 +415,16 @@ describe("FetchHttpApi", () => {
|
||||
it("should refresh token and retry request", async () => {
|
||||
const newAccessToken = "new-access-token";
|
||||
const newRefreshToken = "new-refresh-token";
|
||||
const tokenRefreshFunction = jest.fn().mockResolvedValue({
|
||||
const tokenRefreshFunction = vi.fn().mockResolvedValue({
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
});
|
||||
const fetchFn = jest
|
||||
const fetchFn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(unknownTokenResponse)
|
||||
.mockResolvedValueOnce(okayResponse);
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -355,11 +432,12 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
const result = await api.authedRequest(Method.Post, "/account/password", undefined, undefined, {
|
||||
headers: {},
|
||||
});
|
||||
expect(result).toEqual(okayResponse);
|
||||
expect(result).toEqual({ x: 1 });
|
||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(2);
|
||||
@@ -378,7 +456,7 @@ describe("FetchHttpApi", () => {
|
||||
// count because it's only to get a token with an expiry)
|
||||
const newAccessToken = "new-access-token";
|
||||
const newRefreshToken = "new-refresh-token";
|
||||
const tokenRefreshFunction = jest.fn().mockReturnValue({
|
||||
const tokenRefreshFunction = vi.fn().mockReturnValue({
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
// This needs to be sufficiently high that it's over the threshold for
|
||||
@@ -387,10 +465,10 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
// fetch doesn't like our new or old tokens
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -398,8 +476,9 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
|
||||
unknownTokenErr,
|
||||
);
|
||||
|
||||
@@ -421,7 +500,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
// first refresh is to get a token with an expiry at all, because we
|
||||
// can't specify an expiry on the token we inject
|
||||
const tokenRefreshFunction = jest.fn().mockResolvedValueOnce({
|
||||
const tokenRefreshFunction = vi.fn().mockResolvedValueOnce({
|
||||
accessToken: newAccessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
expiry: new Date(Date.now() + 1000),
|
||||
@@ -442,10 +521,10 @@ describe("FetchHttpApi", () => {
|
||||
expiry: new Date(Date.now() + 5 * 60 * 1000),
|
||||
});
|
||||
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const fetchFn = vi.fn().mockResolvedValue(unknownTokenResponse);
|
||||
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
vi.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
@@ -453,8 +532,9 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrowError(
|
||||
unknownTokenErr,
|
||||
);
|
||||
|
||||
@@ -471,9 +551,9 @@ describe("FetchHttpApi", () => {
|
||||
const localBaseUrl = "http://baseurl";
|
||||
const baseUrlWithTrailingSlash = "http://baseurl/";
|
||||
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
|
||||
const fetchFn = jest.fn();
|
||||
const fetchFn = vi.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn });
|
||||
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn, onlyData: true });
|
||||
};
|
||||
|
||||
type TestParams = {
|
||||
@@ -524,9 +604,15 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
describe("extraParams handling", () => {
|
||||
const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => {
|
||||
const fetchFn = jest.fn();
|
||||
const fetchFn = vi.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
return new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, extraParams });
|
||||
return new FetchHttpApi(emitter, {
|
||||
baseUrl: localBaseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
onlyData: true,
|
||||
extraParams,
|
||||
});
|
||||
};
|
||||
|
||||
const userId = "@rsb-tbg:localhost";
|
||||
@@ -577,9 +663,9 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should work when extraParams is undefined", () => {
|
||||
const fetchFn = jest.fn();
|
||||
const fetchFn = vi.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn });
|
||||
const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, onlyData: true });
|
||||
|
||||
const queryParams = { userId: "123" };
|
||||
const result = api.getUrl("/test", queryParams);
|
||||
@@ -601,21 +687,22 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should not log query parameters", async () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
const responseResolvers = Promise.withResolvers<Response>();
|
||||
const fetchFn = jest.fn().mockReturnValue(responseResolvers.promise);
|
||||
const fetchFn = vi.fn().mockReturnValue(responseResolvers.promise);
|
||||
const mockLogger = {
|
||||
debug: jest.fn(),
|
||||
debug: vi.fn(),
|
||||
} as unknown as Mocked<Logger>;
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
logger: mockLogger,
|
||||
onlyData: true,
|
||||
});
|
||||
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
|
||||
jest.advanceTimersByTime(1234);
|
||||
responseResolvers.resolve({ ok: true, status: 200, text: () => Promise.resolve("RESPONSE") } as Response);
|
||||
vi.advanceTimersByTime(1234);
|
||||
responseResolvers.resolve({ ok: true, status: 200, json: () => Promise.resolve("RESPONSE") } as Response);
|
||||
await prom;
|
||||
expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
|
||||
expect(mockLogger.debug).not.toHaveBeenCalledWith("query");
|
||||
@@ -635,7 +722,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
it("should not make multiple concurrent refresh token requests", async () => {
|
||||
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
|
||||
const fetchFn = jest.fn().mockResolvedValue({
|
||||
const fetchFn = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: tokenInactiveError.httpStatus,
|
||||
async text() {
|
||||
@@ -645,10 +732,10 @@ describe("FetchHttpApi", () => {
|
||||
return tokenInactiveError.data;
|
||||
},
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue("application/json"),
|
||||
get: vi.fn().mockReturnValue("application/json"),
|
||||
},
|
||||
});
|
||||
const tokenRefreshFunction = jest.fn().mockReturnValue(deferredTokenRefresh.promise);
|
||||
const tokenRefreshFunction = vi.fn().mockReturnValue(deferredTokenRefresh.promise);
|
||||
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
@@ -658,6 +745,7 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken: "ACCESS_TOKEN",
|
||||
refreshToken: "REFRESH_TOKEN",
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
const prom1 = api.authedRequest(Method.Get, "/path1");
|
||||
@@ -675,7 +763,7 @@ describe("FetchHttpApi", () => {
|
||||
return {};
|
||||
},
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue("application/json"),
|
||||
get: vi.fn().mockReturnValue("application/json"),
|
||||
},
|
||||
});
|
||||
deferredTokenRefresh.resolve({ accessToken: "NEW_ACCESS_TOKEN", refreshToken: "NEW_REFRESH_TOKEN" });
|
||||
@@ -690,7 +778,7 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
it("should use newly refreshed token if request starts mid-refresh", async () => {
|
||||
const deferredTokenRefresh = Promise.withResolvers<{ accessToken: string; refreshToken: string }>();
|
||||
const fetchFn = jest.fn().mockResolvedValue({
|
||||
const fetchFn = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: tokenInactiveError.httpStatus,
|
||||
async text() {
|
||||
@@ -700,10 +788,10 @@ describe("FetchHttpApi", () => {
|
||||
return tokenInactiveError.data;
|
||||
},
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue("application/json"),
|
||||
get: vi.fn().mockReturnValue("application/json"),
|
||||
},
|
||||
});
|
||||
const tokenRefreshFunction = jest.fn().mockReturnValue(deferredTokenRefresh.promise);
|
||||
const tokenRefreshFunction = vi.fn().mockReturnValue(deferredTokenRefresh.promise);
|
||||
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
@@ -713,6 +801,7 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken: "ACCESS_TOKEN",
|
||||
refreshToken: "REFRESH_TOKEN",
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
const prom1 = api.authedRequest(Method.Get, "/path1");
|
||||
@@ -732,7 +821,7 @@ describe("FetchHttpApi", () => {
|
||||
return {};
|
||||
},
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue("application/json"),
|
||||
get: vi.fn().mockReturnValue("application/json"),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -750,3 +839,7 @@ describe("FetchHttpApi", () => {
|
||||
expect(api.opts.refreshToken).toBe("NEW_REFRESH_TOKEN");
|
||||
});
|
||||
});
|
||||
|
||||
function makeMockFetchFn(): MockedFunction<Window["fetch"]> {
|
||||
return vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
|
||||
}
|
||||
|
||||
@@ -14,66 +14,71 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { ClientPrefix, MatrixHttpApi, Method, type UploadResponse } from "../../../src";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
|
||||
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
describe("MatrixHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
const prefix = ClientPrefix.V3;
|
||||
|
||||
let xhr: Writeable<XMLHttpRequest>;
|
||||
let upload: Promise<UploadResponse>;
|
||||
|
||||
const DONE = 0;
|
||||
|
||||
function getRequest(): Writeable<XMLHttpRequest> | undefined {
|
||||
return vi.mocked(globalThis.XMLHttpRequest)?.mock.instances.at(-1);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
xhr = {
|
||||
upload: {} as XMLHttpRequestUpload,
|
||||
open: jest.fn(),
|
||||
send: jest.fn(),
|
||||
abort: jest.fn(),
|
||||
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
|
||||
// We stub out XHR here as it is not available in the test environment
|
||||
// @ts-ignore
|
||||
globalThis.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
|
||||
globalThis.XMLHttpRequest = vi.fn().mockImplementation(function (this: XMLHttpRequest) {
|
||||
// @ts-ignore
|
||||
this.upload = {} as XMLHttpRequestUpload;
|
||||
this.open = vi.fn();
|
||||
this.send = vi.fn();
|
||||
this.abort = vi.fn();
|
||||
this.setRequestHeader = vi.fn();
|
||||
// @ts-ignore
|
||||
this.onreadystatechange = undefined;
|
||||
this.getResponseHeader = vi.fn();
|
||||
this.getAllResponseHeaders = vi.fn();
|
||||
});
|
||||
// @ts-ignore
|
||||
globalThis.XMLHttpRequest.DONE = DONE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
upload?.catch(() => {});
|
||||
// Abort any remaining requests
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 0;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
const xhr = getRequest();
|
||||
if (xhr) {
|
||||
// Abort any remaining requests
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 0;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
}
|
||||
});
|
||||
|
||||
it("should fall back to `fetch` where xhr is unavailable", async () => {
|
||||
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 });
|
||||
const fetchFn = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({}) });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
await upload;
|
||||
expect(fetchFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should prefer xhr where available", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
const fetchFn = vi.fn().mockResolvedValue({ ok: true });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(fetchFn).not.toHaveBeenCalled();
|
||||
expect(xhr.open).toHaveBeenCalled();
|
||||
expect(getRequest()!.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should send access token in query params if header disabled", () => {
|
||||
@@ -82,13 +87,14 @@ describe("MatrixHttpApi", () => {
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
onlyData: true,
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?access_token=token",
|
||||
);
|
||||
expect(xhr.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
|
||||
expect(getRequest()!.setRequestHeader).not.toHaveBeenCalledWith("Authorization");
|
||||
});
|
||||
|
||||
it("should send access token in header by default", () => {
|
||||
@@ -96,131 +102,138 @@ describe("MatrixHttpApi", () => {
|
||||
baseUrl,
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
onlyData: true,
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
expect(xhr.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
||||
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
|
||||
);
|
||||
expect(getRequest()!.setRequestHeader).toHaveBeenCalledWith("Authorization", "Bearer token");
|
||||
});
|
||||
|
||||
it("should include filename by default", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File, { name: "name" });
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload?filename=name",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow not sending the filename", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
expect(getRequest()!.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
baseUrl.toLowerCase() + "/_matrix/media/v3/upload",
|
||||
);
|
||||
});
|
||||
|
||||
it("should abort xhr when the upload is aborted", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
api.cancelUpload(upload);
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
expect(getRequest()!.abort).toHaveBeenCalled();
|
||||
return expect(upload).rejects.toThrow("Aborted");
|
||||
});
|
||||
|
||||
it("should timeout if no progress in 30s", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
jest.advanceTimersByTime(25000);
|
||||
vi.advanceTimersByTime(25000);
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
|
||||
jest.advanceTimersByTime(25000);
|
||||
expect(xhr.abort).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
getRequest()!.upload.onprogress(new Event("progress", { loaded: 1, total: 100 }));
|
||||
vi.advanceTimersByTime(25000);
|
||||
expect(getRequest()!.abort).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(getRequest()!.abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call progressHandler", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const progressHandler = jest.fn();
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
const progressHandler = vi.fn();
|
||||
upload = api.uploadContent({} as File, { progressHandler });
|
||||
const progressEvent = new Event("progress") as ProgressEvent;
|
||||
Object.assign(progressEvent, { loaded: 1, total: 100 });
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(progressEvent);
|
||||
getRequest()!.upload.onprogress(progressEvent);
|
||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 1, total: 100 });
|
||||
|
||||
Object.assign(progressEvent, { loaded: 95, total: 100 });
|
||||
// @ts-ignore
|
||||
xhr.upload.onprogress(progressEvent);
|
||||
getRequest()!.upload.onprogress(progressEvent);
|
||||
expect(progressHandler).toHaveBeenCalledWith({ loaded: 95, total: 100 });
|
||||
});
|
||||
|
||||
it("should error when no response body", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = "";
|
||||
xhr.status = 200;
|
||||
getRequest()!.readyState = DONE;
|
||||
getRequest()!.responseText = "";
|
||||
getRequest()!.status = 200;
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).rejects.toThrow("No response body.");
|
||||
});
|
||||
|
||||
it("should error on a 400-code", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
||||
xhr.status = 404;
|
||||
mocked(xhr.getResponseHeader).mockImplementation((name) =>
|
||||
getRequest()!.readyState = DONE;
|
||||
getRequest()!.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
||||
getRequest()!.status = 404;
|
||||
vi.mocked(getRequest()!.getResponseHeader).mockImplementation((name) =>
|
||||
name.toLowerCase() === "content-type" ? "application/json" : null,
|
||||
);
|
||||
mocked(xhr.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
|
||||
vi.mocked(getRequest()!.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).rejects.toThrow("Not found");
|
||||
});
|
||||
|
||||
it("should return response on successful upload", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"content_uri": "mxc://server/foobar"}';
|
||||
xhr.status = 200;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
getRequest()!.readyState = DONE;
|
||||
getRequest()!.responseText = '{"content_uri": "mxc://server/foobar"}';
|
||||
getRequest()!.status = 200;
|
||||
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||
|
||||
return expect(upload).resolves.toStrictEqual({ content_uri: "mxc://server/foobar" });
|
||||
});
|
||||
|
||||
it("should abort xhr when calling `cancelUpload`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(api.cancelUpload(upload)).toBeTruthy();
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
expect(getRequest()!.abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
xhr.status = 500;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
getRequest()!.readyState = DONE;
|
||||
getRequest()!.status = 500;
|
||||
vi.mocked(getRequest()!.getResponseHeader).mockReturnValue("application/json");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
getRequest()!.onreadystatechange?.(new Event("test"));
|
||||
await upload.catch(() => {});
|
||||
|
||||
expect(api.cancelUpload(upload)).toBeFalsy();
|
||||
expect(xhr.abort).not.toHaveBeenCalled();
|
||||
expect(getRequest()!.abort).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return active uploads in `getCurrentUploads`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeTruthy();
|
||||
api.cancelUpload(upload);
|
||||
@@ -228,7 +241,12 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should return expected object from `getContentUri`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, accessToken: "token" });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
onlyData: true,
|
||||
});
|
||||
expect(api.getContentUri()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,51 +14,51 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
anySignal,
|
||||
ConnectionError,
|
||||
HTTPError,
|
||||
MatrixError,
|
||||
MatrixSafetyError,
|
||||
MatrixSafetyErrorCode,
|
||||
parseErrorResponse,
|
||||
retryNetworkOperation,
|
||||
timeoutSignal,
|
||||
} from "../../../src";
|
||||
import { sleep } from "../../../src/utils";
|
||||
|
||||
jest.mock("../../../src/utils");
|
||||
vi.mock("../../../src/utils");
|
||||
// setupTests mocks `timeoutSignal` due to hanging timers
|
||||
jest.unmock("../../../src/http-api/utils");
|
||||
vi.unmock("../../../src/http-api/utils");
|
||||
|
||||
describe("timeoutSignal", () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
it("should fire abort signal after specified timeout", () => {
|
||||
const signal = timeoutSignal(3000);
|
||||
const onabort = jest.fn();
|
||||
const onabort = vi.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
expect(onabort).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("anySignal", () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
it("should fire when any signal fires", () => {
|
||||
const { signal } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
|
||||
|
||||
const onabort = jest.fn();
|
||||
const onabort = vi.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(signal.aborted).toBeTruthy();
|
||||
expect(onabort).toHaveBeenCalled();
|
||||
});
|
||||
@@ -66,13 +66,13 @@ describe("anySignal", () => {
|
||||
it("should cleanup when instructed", () => {
|
||||
const { signal, cleanup } = anySignal([timeoutSignal(3000), timeoutSignal(2000)]);
|
||||
|
||||
const onabort = jest.fn();
|
||||
const onabort = vi.fn();
|
||||
signal.onabort = onabort;
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
|
||||
cleanup();
|
||||
jest.advanceTimersByTime(2000);
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(signal.aborted).toBeFalsy();
|
||||
expect(onabort).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -86,9 +86,14 @@ describe("anySignal", () => {
|
||||
});
|
||||
|
||||
describe("parseErrorResponse", () => {
|
||||
const url = "https://example.org";
|
||||
|
||||
let headers: Headers;
|
||||
const xhrHeaderMethods = {
|
||||
getResponseHeader: (name: string) => headers.get(name),
|
||||
responseURL: url,
|
||||
getResponseHeader: (name: string) => {
|
||||
headers.get(name);
|
||||
},
|
||||
getAllResponseHeaders: () => {
|
||||
let allHeaders = "";
|
||||
headers.forEach((value, key) => {
|
||||
@@ -118,6 +123,9 @@ describe("parseErrorResponse", () => {
|
||||
errcode: "TEST",
|
||||
},
|
||||
500,
|
||||
url,
|
||||
undefined,
|
||||
expect.any(Headers),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -127,6 +135,7 @@ describe("parseErrorResponse", () => {
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
url,
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
@@ -138,6 +147,9 @@ describe("parseErrorResponse", () => {
|
||||
errcode: "TEST",
|
||||
},
|
||||
500,
|
||||
url,
|
||||
undefined,
|
||||
expect.any(Headers),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -147,8 +159,8 @@ describe("parseErrorResponse", () => {
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
responseURL: "https://example.com",
|
||||
...xhrHeaderMethods,
|
||||
responseURL: "https://example.com",
|
||||
status: 500,
|
||||
} as XMLHttpRequest,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -160,6 +172,8 @@ describe("parseErrorResponse", () => {
|
||||
},
|
||||
500,
|
||||
"https://example.com",
|
||||
undefined,
|
||||
expect.any(Headers),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -182,9 +196,40 @@ describe("parseErrorResponse", () => {
|
||||
},
|
||||
500,
|
||||
"https://example.com",
|
||||
undefined,
|
||||
expect.any(Headers),
|
||||
),
|
||||
);
|
||||
});
|
||||
it.each([
|
||||
{
|
||||
errcode: MatrixSafetyErrorCode.name,
|
||||
error: "Spammy",
|
||||
},
|
||||
{
|
||||
errcode: MatrixSafetyErrorCode.name,
|
||||
error: "Spammy",
|
||||
expiry: 5000,
|
||||
},
|
||||
{
|
||||
errcode: MatrixSafetyErrorCode.name,
|
||||
error: "Spammy",
|
||||
harms: ["m.spam", "org.example.additional-harm"],
|
||||
expiry: 5000,
|
||||
},
|
||||
])("should resolve MatrixSafetyErrors from fetch", (errContent) => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
const value = parseErrorResponse(
|
||||
{
|
||||
headers,
|
||||
status: 400,
|
||||
} as Response,
|
||||
JSON.stringify(errContent),
|
||||
) as MatrixSafetyError;
|
||||
expect(value).toBeInstanceOf(MatrixSafetyError);
|
||||
expect(value.harms.size).toEqual(errContent.harms?.length ?? 0);
|
||||
expect(value.expiry?.getTime()).toEqual(errContent.expiry);
|
||||
});
|
||||
|
||||
describe("with HTTP headers", () => {
|
||||
function addHeaders(headers: Headers) {
|
||||
@@ -196,7 +241,7 @@ describe("parseErrorResponse", () => {
|
||||
}
|
||||
|
||||
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) {
|
||||
expect(new Map(otherHeaders)).toEqual(new Map(expectedHeaders));
|
||||
expect(new Map(otherHeaders as any)).toEqual(new Map(expectedHeaders as any));
|
||||
}
|
||||
|
||||
it("should resolve HTTP Errors from XHR with headers", () => {
|
||||
@@ -265,7 +310,7 @@ describe("parseErrorResponse", () => {
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
),
|
||||
).toStrictEqual(new HTTPError("Server returned 500 error", 500));
|
||||
).toStrictEqual(new HTTPError("Server returned 500 error", 500, expect.any(Headers)));
|
||||
});
|
||||
|
||||
it("should handle empty type gracefully", () => {
|
||||
@@ -304,27 +349,27 @@ describe("parseErrorResponse", () => {
|
||||
} as Response,
|
||||
"I'm a teapot",
|
||||
),
|
||||
).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418));
|
||||
).toStrictEqual(new HTTPError("Server returned 418 error: I'm a teapot", 418, expect.any(Headers)));
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryNetworkOperation", () => {
|
||||
it("should retry given number of times with exponential sleeps", async () => {
|
||||
const err = new ConnectionError("test");
|
||||
const fn = jest.fn().mockRejectedValue(err);
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
const fn = vi.fn().mockRejectedValue(err);
|
||||
vi.mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(4, fn)).rejects.toThrow(err);
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
expect(mocked(sleep)).toHaveBeenCalledTimes(3);
|
||||
expect(mocked(sleep).mock.calls[0][0]).toBe(2000);
|
||||
expect(mocked(sleep).mock.calls[1][0]).toBe(4000);
|
||||
expect(mocked(sleep).mock.calls[2][0]).toBe(8000);
|
||||
expect(vi.mocked(sleep)).toHaveBeenCalledTimes(3);
|
||||
expect(vi.mocked(sleep).mock.calls[0][0]).toBe(2000);
|
||||
expect(vi.mocked(sleep).mock.calls[1][0]).toBe(4000);
|
||||
expect(vi.mocked(sleep).mock.calls[2][0]).toBe(8000);
|
||||
});
|
||||
|
||||
it("should bail out on errors other than ConnectionError", async () => {
|
||||
const err = new TypeError("invalid JSON");
|
||||
const fn = jest.fn().mockRejectedValue(err);
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
const fn = vi.fn().mockRejectedValue(err);
|
||||
vi.mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -334,10 +379,10 @@ describe("retryNetworkOperation", () => {
|
||||
const err2 = new ConnectionError("test2");
|
||||
const err3 = new ConnectionError("test3");
|
||||
const errors = [err1, err2, err3];
|
||||
const fn = jest.fn().mockImplementation(() => {
|
||||
const fn = vi.fn().mockImplementation(() => {
|
||||
throw errors.shift();
|
||||
});
|
||||
mocked(sleep).mockResolvedValue(undefined);
|
||||
vi.mocked(sleep).mockResolvedValue(undefined);
|
||||
await expect(retryNetworkOperation(3, fn)).rejects.toThrow(err3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
|
||||
import { type MatrixClient } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
|
||||
import { InteractiveAuth, AuthType, NoAuthFlowFoundError } from "../../src/interactive-auth";
|
||||
import { HTTPError, MatrixError } from "../../src/http-api";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { secureRandomString } from "../../src/randomstring";
|
||||
@@ -34,14 +34,14 @@ const getFakeClient = (): MatrixClient => new FakeClient() as unknown as MatrixC
|
||||
|
||||
describe("InteractiveAuth", () => {
|
||||
it("should start an auth stage and complete it", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
@@ -83,14 +83,14 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should handle auth errcode presence", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
@@ -132,9 +132,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should handle set emailSid for email flow", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
doRequest,
|
||||
@@ -186,9 +186,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should make a request if no authdata is provided", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -248,9 +248,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should make a request if authdata is null", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -310,9 +310,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -337,13 +337,15 @@ describe("InteractiveAuth", () => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
|
||||
new NoAuthFlowFoundError("No appropriate authentication flow found", [], []),
|
||||
);
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow but has session", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -372,13 +374,15 @@ describe("InteractiveAuth", () => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("No appropriate authentication flow found"));
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(
|
||||
new NoAuthFlowFoundError("No appropriate authentication flow found", [], []),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle unexpected error types without data property set", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -397,13 +401,13 @@ describe("InteractiveAuth", () => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new Error("myerror"));
|
||||
await expect(ia.attemptAuth.bind(ia)).rejects.toThrow(new HTTPError("myerror", 401));
|
||||
});
|
||||
|
||||
it("should allow dummy auth", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
@@ -435,9 +439,9 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
describe("requestEmailToken", () => {
|
||||
it("increases auth attempts", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
@@ -464,9 +468,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("passes errors through", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
requestEmailToken.mockImplementation(async () => {
|
||||
throw new Error("unspecific network error");
|
||||
});
|
||||
@@ -482,9 +486,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("only starts one request at a time", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
requestEmailToken.mockImplementation(() => sleep(500, { sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
@@ -499,9 +503,9 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("stores result in email sid", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
const requestEmailToken = vi.fn();
|
||||
const sid = secureRandomString(24);
|
||||
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
|
||||
|
||||
@@ -518,14 +522,14 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should prioritise shorter flows", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Recaptcha, AuthType.Password] }, { stages: [AuthType.Password] }],
|
||||
@@ -539,14 +543,14 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should prioritise flows with entirely supported stages", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: ["com.devture.shared_secret_auth"] }, { stages: [AuthType.Password] }],
|
||||
@@ -561,14 +565,14 @@ describe("InteractiveAuth", () => {
|
||||
});
|
||||
|
||||
it("should fire stateUpdated callback with error when a request fails", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const doRequest = vi.fn();
|
||||
const stateUpdated = vi.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
requestEmailToken: jest.fn(),
|
||||
requestEmailToken: vi.fn(),
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [{ stages: [AuthType.Password] }],
|
||||
|
||||
@@ -23,7 +23,7 @@ let client: MatrixClient;
|
||||
describe("Local notification settings", () => {
|
||||
beforeEach(() => {
|
||||
client = new TestClient("@alice:matrix.org", "123", undefined, undefined, undefined).client;
|
||||
client.setAccountData = jest.fn();
|
||||
client.setAccountData = vi.fn();
|
||||
});
|
||||
|
||||
describe("Lets you set local notification settings", () => {
|
||||
|
||||
@@ -18,15 +18,15 @@ limitations under the License.
|
||||
|
||||
import loglevel from "loglevel";
|
||||
|
||||
import { logger } from "../../src/logger.ts";
|
||||
import { DebugLogger, logger } from "../../src/logger.ts";
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("logger", () => {
|
||||
it("should log to console by default", () => {
|
||||
jest.spyOn(console, "debug").mockReturnValue(undefined);
|
||||
vi.spyOn(console, "debug").mockReturnValue(undefined);
|
||||
logger.debug("test1");
|
||||
logger.log("test2");
|
||||
|
||||
@@ -35,8 +35,8 @@ describe("logger", () => {
|
||||
});
|
||||
|
||||
it("should allow creation of child loggers which add a prefix", () => {
|
||||
jest.spyOn(loglevel, "getLogger");
|
||||
jest.spyOn(console, "debug").mockReturnValue(undefined);
|
||||
vi.spyOn(loglevel, "getLogger");
|
||||
vi.spyOn(console, "debug").mockReturnValue(undefined);
|
||||
|
||||
const childLogger = logger.getChild("[prefix1]");
|
||||
expect(loglevel.getLogger).toHaveBeenCalledWith("matrix-[prefix1]");
|
||||
@@ -49,3 +49,42 @@ describe("logger", () => {
|
||||
expect(console.debug).toHaveBeenCalledWith("[prefix1][prefix2]", "test2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DebugLogger", () => {
|
||||
it("should handle empty log messages", () => {
|
||||
const mockTarget = vi.fn();
|
||||
const logger = new DebugLogger(mockTarget as any);
|
||||
logger.info();
|
||||
expect(mockTarget).toHaveBeenCalledTimes(1);
|
||||
expect(mockTarget).toHaveBeenCalledWith("[INFO] ");
|
||||
});
|
||||
|
||||
it("should handle logging an Error", () => {
|
||||
const mockTarget = vi.fn();
|
||||
const logger = new DebugLogger(mockTarget as any);
|
||||
|
||||
// If there is a stack and a message, we use the stack.
|
||||
const error = new Error("I am an error");
|
||||
logger.error(error);
|
||||
expect(mockTarget).toHaveBeenCalledTimes(1);
|
||||
expect(mockTarget).toHaveBeenCalledWith(expect.stringMatching(/^\[ERROR\] Error: I am an error\n\s*at/));
|
||||
|
||||
mockTarget.mockClear();
|
||||
|
||||
// If there is only a message, we use that.
|
||||
error.stack = undefined;
|
||||
logger.error(error);
|
||||
expect(mockTarget).toHaveBeenCalledTimes(1);
|
||||
expect(mockTarget).toHaveBeenCalledWith("[ERROR] I am an error");
|
||||
});
|
||||
|
||||
it("should handle logging an object", () => {
|
||||
const mockTarget = vi.fn();
|
||||
const logger = new DebugLogger(mockTarget as any);
|
||||
|
||||
const obj = { a: 1 };
|
||||
logger.warn(obj);
|
||||
expect(mockTarget).toHaveBeenCalledTimes(1);
|
||||
expect(mockTarget).toHaveBeenCalledWith("[WARN] %O", obj);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { ClientPrefix, MatrixClient } from "../../src";
|
||||
import { SSOAction } from "../../src/@types/auth";
|
||||
@@ -51,27 +51,26 @@ describe("SSO login URL", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
|
||||
expect(url.searchParams.has("action")).toBe(false);
|
||||
});
|
||||
|
||||
it("register", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
|
||||
expect(url.searchParams.get("action")).toEqual("register");
|
||||
});
|
||||
|
||||
it("login", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
|
||||
expect(url.searchParams.get("action")).toEqual("login");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshToken", () => {
|
||||
afterEach(() => {
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
|
||||
+520
-173
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023-2026 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.
|
||||
@@ -14,65 +14,71 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MatrixEvent } from "../../../src";
|
||||
import {
|
||||
CallMembership,
|
||||
type SessionMembershipData,
|
||||
DEFAULT_EXPIRE_DURATION,
|
||||
} from "../../../src/matrixrtc/CallMembership";
|
||||
import { membershipTemplate } from "./mocks";
|
||||
import { type RtcMembershipData, type SessionMembershipData } from "../../../src/matrixrtc/membershipData/index.ts";
|
||||
import { type IContent, type MatrixEvent } from "../../../src/models/event.ts";
|
||||
import { EventType } from "../../../src/@types/event.ts";
|
||||
import { CallMembership, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership.ts";
|
||||
|
||||
function makeMockEvent(originTs = 0): MatrixEvent {
|
||||
return {
|
||||
getTs: jest.fn().mockReturnValue(originTs),
|
||||
getSender: jest.fn().mockReturnValue("@alice:example.org"),
|
||||
} as unknown as MatrixEvent;
|
||||
function createCallMembership(ev: MatrixEvent, content: IContent): CallMembership {
|
||||
vi.mocked(ev.getContent).mockReturnValue(content);
|
||||
const data = CallMembership.membershipDataFromMatrixEvent(ev);
|
||||
return new CallMembership(ev, data, "xx");
|
||||
}
|
||||
|
||||
describe("CallMembership", () => {
|
||||
describe("SessionMembershipData", () => {
|
||||
function makeMockEvent(originTs = 0): MatrixEvent {
|
||||
return {
|
||||
getTs: vi.fn().mockReturnValue(originTs),
|
||||
getSender: vi.fn().mockReturnValue("@alice:example.org"),
|
||||
getId: vi.fn().mockReturnValue("$eventid"),
|
||||
getContent: vi.fn().mockReturnValue({}),
|
||||
getType: vi.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const membershipTemplate: SessionMembershipData = {
|
||||
call_id: "",
|
||||
scope: "m.room",
|
||||
application: "m.call",
|
||||
device_id: "AAAAAAA",
|
||||
focus_active: { type: "livekit" },
|
||||
foci_preferred: [{ type: "livekit" }],
|
||||
"call_id": "",
|
||||
"scope": "m.room",
|
||||
"application": "m.call",
|
||||
"device_id": "AAAAAAA",
|
||||
"focus_active": { type: "livekit", focus_selection: "oldest_membership" },
|
||||
"foci_preferred": [{ type: "livekit" }],
|
||||
"m.call.intent": "voice",
|
||||
};
|
||||
|
||||
it("rejects membership with no device_id", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
||||
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("rejects membership with no call_id", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
||||
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("allow membership with no scope", () => {
|
||||
expect(() => {
|
||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
||||
createCallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("uses event timestamp if no created_ts", () => {
|
||||
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
|
||||
const membership = createCallMembership(makeMockEvent(12345), membershipTemplate);
|
||||
expect(membership.createdTs()).toEqual(12345);
|
||||
});
|
||||
|
||||
it("uses created_ts if present", () => {
|
||||
const membership = new CallMembership(
|
||||
const membership = createCallMembership(
|
||||
makeMockEvent(12345),
|
||||
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
|
||||
);
|
||||
@@ -81,47 +87,347 @@ describe("CallMembership", () => {
|
||||
|
||||
it("considers memberships unexpired if local age low enough", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
|
||||
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
|
||||
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
|
||||
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
|
||||
});
|
||||
|
||||
it("considers memberships expired if local age large enough", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
|
||||
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
|
||||
fakeEvent.getTs = vi.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
|
||||
expect(createCallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns preferred foci", () => {
|
||||
const fakeEvent = makeMockEvent();
|
||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||
const membership = new CallMembership(
|
||||
fakeEvent,
|
||||
Object.assign({}, membershipTemplate, { foci_preferred: [mockFocus] }),
|
||||
);
|
||||
expect(membership.getPreferredFoci()).toEqual([mockFocus]);
|
||||
const membership = createCallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
|
||||
expect(membership.transports).toEqual([mockFocus]);
|
||||
});
|
||||
|
||||
describe("getTransport", () => {
|
||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||
const oldestMembership = createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
it("gets the correct active transport with oldest_membership", () => {
|
||||
const membership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
foci_preferred: [mockFocus],
|
||||
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
||||
});
|
||||
|
||||
// if we are the oldest member we use our focus.
|
||||
expect(membership.getTransport(membership)).toStrictEqual(mockFocus);
|
||||
|
||||
// If there is an older member we use its focus.
|
||||
expect(membership.getTransport(oldestMembership)).toBe(membershipTemplate.foci_preferred[0]);
|
||||
});
|
||||
|
||||
it("gets the correct active transport with multi_sfu", () => {
|
||||
const membership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
foci_preferred: [mockFocus],
|
||||
focus_active: { type: "livekit", focus_selection: "multi_sfu" },
|
||||
});
|
||||
|
||||
// if we are the oldest member we use our focus.
|
||||
expect(membership.getTransport(membership)).toStrictEqual(mockFocus);
|
||||
|
||||
// If there is an older member we still use our own focus in multi sfu.
|
||||
expect(membership.getTransport(oldestMembership)).toBe(mockFocus);
|
||||
});
|
||||
it("does not provide focus if the selection method is unknown", () => {
|
||||
const membership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
foci_preferred: [mockFocus],
|
||||
focus_active: { type: "livekit", focus_selection: "unknown" },
|
||||
});
|
||||
|
||||
// if we are the oldest member we use our focus.
|
||||
expect(membership.getTransport(membership)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe("correct values from computed fields", () => {
|
||||
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
it("returns correct sender", () => {
|
||||
expect(membership.sender).toBe("@alice:example.org");
|
||||
});
|
||||
it("returns correct eventId", () => {
|
||||
expect(membership.eventId).toBe("$eventid");
|
||||
});
|
||||
it("returns correct slot_id", () => {
|
||||
// slot_id is application and call_id dependent. So we create
|
||||
// a membership for each possible combination
|
||||
|
||||
// non call application (should not alter call_id even with empty string)
|
||||
const nonCallMembership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
application: "m.not.a.call",
|
||||
call_id: "",
|
||||
});
|
||||
// non "" call id should not be altered
|
||||
const callMembershipCustomId = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
call_id: "customCallId",
|
||||
});
|
||||
|
||||
// for membership (application = m.call and call_id = "") we expect "" -> ROOM
|
||||
// for legacy events we expect the room to be added automagically
|
||||
// See INFO_SLOT_ID_LEGACY_CASE comments
|
||||
expect(membership.slotId).toBe("m.call#ROOM");
|
||||
expect(membership.slotDescription).toStrictEqual({ id: "ROOM", application: "m.call" });
|
||||
|
||||
expect(nonCallMembership.slotId).toBe("m.not.a.call#");
|
||||
expect(nonCallMembership.slotDescription).toStrictEqual({ id: "", application: "m.not.a.call" });
|
||||
|
||||
expect(callMembershipCustomId.slotId).toBe("m.call#customCallId");
|
||||
expect(callMembershipCustomId.slotDescription).toStrictEqual({
|
||||
id: "customCallId",
|
||||
application: "m.call",
|
||||
});
|
||||
});
|
||||
it("returns correct deviceId", () => {
|
||||
expect(membership.deviceId).toBe("AAAAAAA");
|
||||
});
|
||||
it("returns correct call intent", () => {
|
||||
expect(membership.callIntent).toBe("voice");
|
||||
});
|
||||
it("returns correct application", () => {
|
||||
expect(membership.application).toStrictEqual("m.call");
|
||||
});
|
||||
it("returns correct applicationData", () => {
|
||||
expect(membership.applicationData).toStrictEqual({ "type": "m.call", "m.call.intent": "voice" });
|
||||
});
|
||||
it("returns correct scope", () => {
|
||||
expect(membership.scope).toBe("m.room");
|
||||
});
|
||||
it("returns correct membershipID", () => {
|
||||
expect(membership.membershipID).toBe("@alice:example.org:AAAAAAA");
|
||||
});
|
||||
it("returns correct unused fields", () => {
|
||||
expect(membership.getAbsoluteExpiry()).toBe(DEFAULT_EXPIRE_DURATION);
|
||||
expect(membership.getMsUntilExpiry()).toBe(DEFAULT_EXPIRE_DURATION - Date.now());
|
||||
expect(membership.isExpired()).toBe(true);
|
||||
});
|
||||
});
|
||||
describe("expiry calculation", () => {
|
||||
let fakeEvent: MatrixEvent;
|
||||
let membership: CallMembership;
|
||||
|
||||
beforeEach(() => {
|
||||
// server origin timestamp for this event is 1000
|
||||
fakeEvent = makeMockEvent(1000);
|
||||
membership = createCallMembership(fakeEvent!, membershipTemplate);
|
||||
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it("calculates time until expiry", () => {
|
||||
vi.setSystemTime(2000);
|
||||
// should be using absolute expiry time
|
||||
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("expiry calculation", () => {
|
||||
let fakeEvent: MatrixEvent;
|
||||
let membership: CallMembership;
|
||||
describe("RtcMembershipData", () => {
|
||||
function makeMockEvent(originTs = 0, content: IContent = {}): MatrixEvent {
|
||||
return {
|
||||
getTs: vi.fn().mockReturnValue(originTs),
|
||||
getSender: vi.fn().mockReturnValue("@alice:example.org"),
|
||||
getId: vi.fn().mockReturnValue("$eventid"),
|
||||
getContent: vi.fn().mockReturnValue(content),
|
||||
getType: vi.fn().mockReturnValue(EventType.RTCMembership),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
const membershipTemplate: RtcMembershipData = {
|
||||
slot_id: "m.call#",
|
||||
application: { "type": "m.call", "m.call.id": "", "m.call.intent": "voice" },
|
||||
member: { user_id: "@alice:example.org", device_id: "AAAAAAA", id: "xyzHASHxyz" },
|
||||
rtc_transports: [{ type: "livekit" }],
|
||||
versions: [],
|
||||
msc4354_sticky_key: "abc123",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// server origin timestamp for this event is 1000
|
||||
fakeEvent = makeMockEvent(1000);
|
||||
membership = new CallMembership(fakeEvent!, membershipTemplate);
|
||||
|
||||
jest.useFakeTimers();
|
||||
it("rejects membership with no slot_id", () => {
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
|
||||
}).toThrow();
|
||||
});
|
||||
it("rejects membership with invalid slot_id", () => {
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "invalid_slot_id" });
|
||||
}).toThrow();
|
||||
});
|
||||
it("rejects membership with slot_id that contains extra #", () => {
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#mycall#extra" });
|
||||
}).toThrow();
|
||||
});
|
||||
it("accepts membership with valid slot_id", () => {
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#" });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
it("rejects membership with no application", () => {
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("calculates time until expiry", () => {
|
||||
jest.setSystemTime(2000);
|
||||
// should be using absolute expiry time
|
||||
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
||||
it("rejects membership with incorrect application", () => {
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
application: { wrong_type_key: "unknown" },
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("rejects membership with no member", () => {
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("rejects membership with incorrect member", () => {
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
member: { id: "test", device_id: "test", user_id_wrong: "test" },
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" },
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
member: { id: "test", device_id: "test", user_id: "@@test" },
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
member: { id: "test", device_id: "test", user_id: "@test-wrong-user:user.id" },
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
it("rejects membership with incorrect sticky_key", () => {
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
sticky_key: 1,
|
||||
msc4354_sticky_key: undefined,
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
sticky_key: "1",
|
||||
msc4354_sticky_key: undefined,
|
||||
});
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), { ...membershipTemplate, msc4354_sticky_key: undefined });
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
msc4354_sticky_key: 1,
|
||||
sticky_key: "valid",
|
||||
});
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
msc4354_sticky_key: "valid",
|
||||
sticky_key: "valid",
|
||||
});
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
msc4354_sticky_key: "valid_but_different",
|
||||
sticky_key: "valid",
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
// TODO link prev event
|
||||
it.todo("considers memberships unexpired if local age low enough");
|
||||
it.todo("considers memberships expired if local age large enough");
|
||||
|
||||
describe("getTransport", () => {
|
||||
it("gets the correct active transport with oldest_membership", () => {
|
||||
const oldestMembership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
rtc_transports: [{ type: "oldest_transport" }],
|
||||
});
|
||||
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
|
||||
// if we are the oldest member we use our focus.
|
||||
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });
|
||||
|
||||
// If there is an older member we use our own focus focus. (RtcMembershipData always uses multi sfu)
|
||||
expect(membership.getTransport(oldestMembership)).toStrictEqual({ type: "livekit" });
|
||||
});
|
||||
});
|
||||
describe("correct values from computed fields", () => {
|
||||
const membership = createCallMembership(makeMockEvent(), membershipTemplate);
|
||||
it("returns correct sender", () => {
|
||||
expect(membership.sender).toBe("@alice:example.org");
|
||||
});
|
||||
it("returns correct eventId", () => {
|
||||
expect(membership.eventId).toBe("$eventid");
|
||||
});
|
||||
it("returns correct slot_id", () => {
|
||||
expect(membership.slotId).toBe("m.call#");
|
||||
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
|
||||
});
|
||||
it("returns correct deviceId", () => {
|
||||
expect(membership.deviceId).toBe("AAAAAAA");
|
||||
});
|
||||
it("returns correct call intent", () => {
|
||||
expect(membership.callIntent).toBe("voice");
|
||||
});
|
||||
it("returns correct application", () => {
|
||||
expect(membership.application).toStrictEqual("m.call");
|
||||
});
|
||||
it("returns correct applicationData", () => {
|
||||
expect(membership.applicationData).toStrictEqual({
|
||||
"type": "m.call",
|
||||
"m.call.id": "",
|
||||
"m.call.intent": "voice",
|
||||
});
|
||||
});
|
||||
it("returns correct scope", () => {
|
||||
expect(membership.scope).toBe(undefined);
|
||||
});
|
||||
it("returns correct membershipID", () => {
|
||||
expect(membership.membershipID).toBe("xyzHASHxyz");
|
||||
});
|
||||
it("returns correct unused fields", () => {
|
||||
expect(membership.getAbsoluteExpiry()).toBe(undefined);
|
||||
expect(membership.getMsUntilExpiry()).toBe(undefined);
|
||||
expect(membership.isExpired()).toBe(false);
|
||||
});
|
||||
});
|
||||
it("uses unpadded base64 for RTC backend identities", async () => {
|
||||
const membership = await CallMembership.parseFromEvent(makeMockEvent(0, { ...membershipTemplate }));
|
||||
expect(membership.rtcBackendIdentity).toBe("jUZ0Q1yF5nV3LlAI5xfD1I7BPnAytJaPEAR57EXjJ6s");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+17
-13
@@ -14,47 +14,51 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { isLivekitFocus, isLivekitFocusActive, isLivekitFocusConfig } from "../../../src/matrixrtc/LivekitFocus";
|
||||
import {
|
||||
isLivekitTransport,
|
||||
isLivekitFocusSelection,
|
||||
isLivekitTransportConfig,
|
||||
} from "../../../src/matrixrtc/LivekitTransport";
|
||||
|
||||
describe("LivekitFocus", () => {
|
||||
it("isLivekitFocus", () => {
|
||||
expect(
|
||||
isLivekitFocus({
|
||||
isLivekitTransport({
|
||||
type: "livekit",
|
||||
livekit_service_url: "http://test.com",
|
||||
livekit_alias: "test",
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(isLivekitFocus({ type: "livekit" })).toBeFalsy();
|
||||
expect(isLivekitTransport({ type: "livekit" })).toBeFalsy();
|
||||
expect(
|
||||
isLivekitFocus({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }),
|
||||
isLivekitTransport({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
isLivekitFocus({ type: "livekit", other_service_url: "http://test.com", livekit_alias: "test" }),
|
||||
isLivekitTransport({ type: "livekit", other_service_url: "http://test.com", livekit_alias: "test" }),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
isLivekitFocus({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }),
|
||||
isLivekitTransport({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }),
|
||||
).toBeFalsy();
|
||||
});
|
||||
it("isLivekitFocusActive", () => {
|
||||
expect(
|
||||
isLivekitFocusActive({
|
||||
isLivekitFocusSelection({
|
||||
type: "livekit",
|
||||
focus_selection: "oldest_membership",
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(isLivekitFocusActive({ type: "livekit" })).toBeFalsy();
|
||||
expect(isLivekitFocusActive({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
|
||||
expect(isLivekitFocusSelection({ type: "livekit" })).toBeFalsy();
|
||||
expect(isLivekitFocusSelection({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
|
||||
});
|
||||
it("isLivekitFocusConfig", () => {
|
||||
expect(
|
||||
isLivekitFocusConfig({
|
||||
isLivekitTransportConfig({
|
||||
type: "livekit",
|
||||
livekit_service_url: "http://test.com",
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(isLivekitFocusConfig({ type: "livekit" })).toBeFalsy();
|
||||
expect(isLivekitFocusConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
|
||||
expect(isLivekitFocusConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
|
||||
expect(isLivekitTransportConfig({ type: "livekit" })).toBeFalsy();
|
||||
expect(isLivekitTransportConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
|
||||
expect(isLivekitTransportConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user